@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.
- package/CHANGELOG.md +39 -0
- package/dist/adapters/commands/login.d.ts.map +1 -1
- package/dist/adapters/commands/login.js +0 -9
- package/dist/adapters/commands/login.js.map +1 -1
- package/dist/adapters/commands/manifest.d.ts +0 -19
- package/dist/adapters/commands/manifest.d.ts.map +1 -1
- package/dist/adapters/commands/manifest.js +0 -34
- package/dist/adapters/commands/manifest.js.map +1 -1
- package/dist/adapters/commands/model.d.ts +0 -5
- package/dist/adapters/commands/model.d.ts.map +1 -1
- package/dist/adapters/commands/model.js +0 -5
- package/dist/adapters/commands/model.js.map +1 -1
- package/dist/adapters/commands/new.d.ts.map +1 -1
- package/dist/adapters/commands/new.js +0 -2
- package/dist/adapters/commands/new.js.map +1 -1
- package/dist/adapters/commands/registry.d.ts +0 -1
- package/dist/adapters/commands/registry.d.ts.map +1 -1
- package/dist/adapters/commands/registry.js +0 -8
- package/dist/adapters/commands/registry.js.map +1 -1
- package/dist/adapters/commands/session-view.d.ts.map +1 -1
- package/dist/adapters/commands/session-view.js +0 -2
- package/dist/adapters/commands/session-view.js.map +1 -1
- package/dist/adapters/commands/types.d.ts +0 -29
- package/dist/adapters/commands/types.d.ts.map +1 -1
- package/dist/adapters/commands/types.js.map +1 -1
- package/dist/adapters/commands/utils.d.ts +0 -3
- package/dist/adapters/commands/utils.d.ts.map +1 -1
- package/dist/adapters/commands/utils.js +0 -3
- package/dist/adapters/commands/utils.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +0 -4
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +5 -41
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/components.d.ts +0 -23
- package/dist/adapters/discord/components.d.ts.map +1 -1
- package/dist/adapters/discord/components.js +0 -23
- package/dist/adapters/discord/components.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +1 -11
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/discord/format.d.ts +0 -1
- package/dist/adapters/discord/format.d.ts.map +1 -1
- package/dist/adapters/discord/format.js +0 -37
- package/dist/adapters/discord/format.js.map +1 -1
- package/dist/adapters/github/bot.d.ts +0 -44
- package/dist/adapters/github/bot.d.ts.map +1 -1
- package/dist/adapters/github/bot.js +1 -120
- package/dist/adapters/github/bot.js.map +1 -1
- package/dist/adapters/github/client.d.ts +0 -57
- package/dist/adapters/github/client.d.ts.map +1 -1
- package/dist/adapters/github/client.js +0 -59
- package/dist/adapters/github/client.js.map +1 -1
- package/dist/adapters/github/context.d.ts.map +1 -1
- package/dist/adapters/github/context.js +1 -8
- package/dist/adapters/github/context.js.map +1 -1
- package/dist/adapters/github/github-ops.d.ts +0 -55
- package/dist/adapters/github/github-ops.d.ts.map +1 -1
- package/dist/adapters/github/github-ops.js +0 -59
- package/dist/adapters/github/github-ops.js.map +1 -1
- package/dist/adapters/github/ids.d.ts +0 -27
- package/dist/adapters/github/ids.d.ts.map +1 -1
- package/dist/adapters/github/ids.js +0 -29
- package/dist/adapters/github/ids.js.map +1 -1
- package/dist/adapters/github/repo.d.ts +0 -25
- package/dist/adapters/github/repo.d.ts.map +1 -1
- package/dist/adapters/github/repo.js +0 -49
- package/dist/adapters/github/repo.js.map +1 -1
- package/dist/adapters/github/tool-pack.d.ts +0 -10
- package/dist/adapters/github/tool-pack.d.ts.map +1 -1
- package/dist/adapters/github/tool-pack.js +0 -10
- package/dist/adapters/github/tool-pack.js.map +1 -1
- package/dist/adapters/github/tools/checks.d.ts +0 -6
- package/dist/adapters/github/tools/checks.d.ts.map +1 -1
- package/dist/adapters/github/tools/checks.js +0 -7
- package/dist/adapters/github/tools/checks.js.map +1 -1
- package/dist/adapters/github/tools/issue.d.ts +0 -5
- package/dist/adapters/github/tools/issue.d.ts.map +1 -1
- package/dist/adapters/github/tools/issue.js +0 -5
- package/dist/adapters/github/tools/issue.js.map +1 -1
- package/dist/adapters/github/tools/pr.d.ts +0 -7
- package/dist/adapters/github/tools/pr.d.ts.map +1 -1
- package/dist/adapters/github/tools/pr.js +0 -7
- package/dist/adapters/github/tools/pr.js.map +1 -1
- package/dist/adapters/github/tools/read.d.ts +0 -6
- package/dist/adapters/github/tools/read.d.ts.map +1 -1
- package/dist/adapters/github/tools/read.js +0 -8
- package/dist/adapters/github/tools/read.js.map +1 -1
- package/dist/adapters/github/tools/review-reply.d.ts +0 -5
- package/dist/adapters/github/tools/review-reply.d.ts.map +1 -1
- package/dist/adapters/github/tools/review-reply.js +0 -5
- package/dist/adapters/github/tools/review-reply.js.map +1 -1
- package/dist/adapters/github/tools/sync.d.ts +0 -6
- package/dist/adapters/github/tools/sync.d.ts.map +1 -1
- package/dist/adapters/github/tools/sync.js +0 -6
- package/dist/adapters/github/tools/sync.js.map +1 -1
- package/dist/adapters/github/types.d.ts +0 -88
- package/dist/adapters/github/types.d.ts.map +1 -1
- package/dist/adapters/github/types.js.map +1 -1
- package/dist/adapters/github/webhook.d.ts +0 -13
- package/dist/adapters/github/webhook.d.ts.map +1 -1
- package/dist/adapters/github/webhook.js +0 -16
- package/dist/adapters/github/webhook.js.map +1 -1
- package/dist/adapters/index.d.ts +3 -4
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/intake.d.ts +0 -17
- package/dist/adapters/intake.d.ts.map +1 -1
- package/dist/adapters/intake.js +6 -32
- package/dist/adapters/intake.js.map +1 -1
- package/dist/adapters/markdown-tables.d.ts +0 -10
- package/dist/adapters/markdown-tables.d.ts.map +1 -1
- package/dist/adapters/markdown-tables.js +1 -42
- package/dist/adapters/markdown-tables.js.map +1 -1
- package/dist/adapters/progressive-renderer.d.ts.map +1 -1
- package/dist/adapters/progressive-renderer.js +3 -25
- package/dist/adapters/progressive-renderer.js.map +1 -1
- package/dist/adapters/shared.d.ts +0 -57
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/shared.js +0 -71
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/slack/assistant.d.ts +0 -49
- package/dist/adapters/slack/assistant.d.ts.map +1 -1
- package/dist/adapters/slack/assistant.js +0 -97
- package/dist/adapters/slack/assistant.js.map +1 -1
- package/dist/adapters/slack/auto-reply-context.d.ts +0 -7
- package/dist/adapters/slack/auto-reply-context.d.ts.map +1 -1
- package/dist/adapters/slack/auto-reply-context.js +0 -7
- package/dist/adapters/slack/auto-reply-context.js.map +1 -1
- package/dist/adapters/slack/blocks.d.ts +0 -9
- package/dist/adapters/slack/blocks.d.ts.map +1 -1
- package/dist/adapters/slack/blocks.js +0 -20
- package/dist/adapters/slack/blocks.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +0 -116
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +7 -231
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +1 -3
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/jev-context.d.ts +0 -13
- package/dist/adapters/slack/jev-context.d.ts.map +1 -1
- package/dist/adapters/slack/jev-context.js +0 -12
- package/dist/adapters/slack/jev-context.js.map +1 -1
- package/dist/adapters/slack/markdown.d.ts +0 -1
- package/dist/adapters/slack/markdown.d.ts.map +1 -1
- package/dist/adapters/slack/markdown.js +0 -5
- package/dist/adapters/slack/markdown.js.map +1 -1
- package/dist/adapters/slack/persona.d.ts +0 -9
- package/dist/adapters/slack/persona.d.ts.map +1 -1
- package/dist/adapters/slack/persona.js +0 -9
- package/dist/adapters/slack/persona.js.map +1 -1
- package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
- package/dist/adapters/slack/response-lifecycle.js +1 -45
- package/dist/adapters/slack/response-lifecycle.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +2 -1
- package/dist/adapters/slack/session.d.ts.map +1 -1
- package/dist/adapters/slack/session.js +2 -2
- package/dist/adapters/slack/session.js.map +1 -1
- package/dist/adapters/slack/stream-limits.d.ts +0 -25
- package/dist/adapters/slack/stream-limits.d.ts.map +1 -1
- package/dist/adapters/slack/stream-limits.js +0 -34
- package/dist/adapters/slack/stream-limits.js.map +1 -1
- package/dist/adapters/slack/task-intent.d.ts +0 -13
- package/dist/adapters/slack/task-intent.d.ts.map +1 -1
- package/dist/adapters/slack/task-intent.js +0 -7
- package/dist/adapters/slack/task-intent.js.map +1 -1
- package/dist/adapters/slack/task-status.d.ts +0 -1
- package/dist/adapters/slack/task-status.d.ts.map +1 -1
- package/dist/adapters/slack/task-status.js +1 -9
- package/dist/adapters/slack/task-status.js.map +1 -1
- package/dist/adapters/slack/tool-pack.d.ts +0 -6
- package/dist/adapters/slack/tool-pack.d.ts.map +1 -1
- package/dist/adapters/slack/tool-pack.js +0 -6
- package/dist/adapters/slack/tool-pack.js.map +1 -1
- package/dist/adapters/slack/tools/blockkit.d.ts.map +1 -1
- package/dist/adapters/slack/tools/blockkit.js +0 -11
- package/dist/adapters/slack/tools/blockkit.js.map +1 -1
- package/dist/adapters/slack/types.d.ts +0 -19
- package/dist/adapters/slack/types.d.ts.map +1 -1
- package/dist/adapters/slack/types.js.map +1 -1
- package/dist/adapters/slack/update-diagnostics.d.ts +0 -1
- package/dist/adapters/slack/update-diagnostics.d.ts.map +1 -1
- package/dist/adapters/slack/update-diagnostics.js +1 -7
- package/dist/adapters/slack/update-diagnostics.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +0 -6
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +1 -50
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +1 -8
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/types.d.ts +0 -57
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/adapters/web/admin/client-assets.d.ts +4 -0
- package/dist/adapters/web/admin/client-assets.d.ts.map +1 -0
- package/dist/adapters/web/admin/client-assets.js +1765 -0
- package/dist/adapters/web/admin/client-assets.js.map +1 -0
- package/dist/adapters/web/admin/portal.d.ts +0 -10
- package/dist/adapters/web/admin/portal.d.ts.map +1 -1
- package/dist/adapters/web/admin/portal.js +6 -1909
- package/dist/adapters/web/admin/portal.js.map +1 -1
- package/dist/adapters/web/admin/types.d.ts +0 -3
- package/dist/adapters/web/admin/types.d.ts.map +1 -1
- package/dist/adapters/web/admin/types.js.map +1 -1
- package/dist/adapters/web/login/oauth.d.ts.map +1 -1
- package/dist/adapters/web/login/oauth.js +3 -7
- package/dist/adapters/web/login/oauth.js.map +1 -1
- package/dist/adapters/web/login/portal.d.ts +0 -10
- package/dist/adapters/web/login/portal.d.ts.map +1 -1
- package/dist/adapters/web/login/portal.js +4 -37
- package/dist/adapters/web/login/portal.js.map +1 -1
- package/dist/adapters/web/login/types.d.ts +0 -5
- package/dist/adapters/web/login/types.d.ts.map +1 -1
- package/dist/adapters/web/login/types.js.map +1 -1
- package/dist/adapters/web/portal-shell.d.ts +0 -12
- package/dist/adapters/web/portal-shell.d.ts.map +1 -1
- package/dist/adapters/web/portal-shell.js +8 -25
- package/dist/adapters/web/portal-shell.js.map +1 -1
- package/dist/adapters/web/server.d.ts +0 -1
- package/dist/adapters/web/server.d.ts.map +1 -1
- package/dist/adapters/web/server.js +0 -2
- package/dist/adapters/web/server.js.map +1 -1
- package/dist/adapters/web/session-view/portal.d.ts.map +1 -1
- package/dist/adapters/web/session-view/portal.js +13 -24
- package/dist/adapters/web/session-view/portal.js.map +1 -1
- package/dist/adapters/web/session-view/types.d.ts.map +1 -1
- package/dist/adapters/web/session-view/types.js.map +1 -1
- package/dist/adapters/web/token-store.d.ts +0 -2
- package/dist/adapters/web/token-store.d.ts.map +1 -1
- package/dist/adapters/web/token-store.js +0 -2
- package/dist/adapters/web/token-store.js.map +1 -1
- package/dist/cli/arg-grammar.d.ts +0 -3
- package/dist/cli/arg-grammar.d.ts.map +1 -1
- package/dist/cli/arg-grammar.js +0 -3
- package/dist/cli/arg-grammar.js.map +1 -1
- package/dist/cli/boot.d.ts.map +1 -1
- package/dist/cli/boot.js +3 -12
- package/dist/cli/boot.js.map +1 -1
- package/dist/cli/download.d.ts.map +1 -1
- package/dist/cli/download.js +2 -6
- package/dist/cli/download.js.map +1 -1
- package/dist/cli/office.d.ts.map +1 -1
- package/dist/cli/office.js +0 -31
- package/dist/cli/office.js.map +1 -1
- package/dist/cli/onboard.d.ts +0 -2
- package/dist/cli/onboard.d.ts.map +1 -1
- package/dist/cli/onboard.js +0 -30
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/process-lifecycle.d.ts +0 -5
- package/dist/cli/process-lifecycle.d.ts.map +1 -1
- package/dist/cli/process-lifecycle.js +0 -6
- package/dist/cli/process-lifecycle.js.map +1 -1
- package/dist/cli/sandbox.d.ts +3 -0
- package/dist/cli/sandbox.d.ts.map +1 -0
- package/dist/cli/sandbox.js +79 -0
- package/dist/cli/sandbox.js.map +1 -0
- package/dist/cli/sessions.d.ts.map +1 -1
- package/dist/cli/sessions.js +0 -11
- package/dist/cli/sessions.js.map +1 -1
- package/dist/cli/types.d.ts +2 -6
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/env-manifest.d.ts +0 -21
- package/dist/env-manifest.d.ts.map +1 -1
- package/dist/env-manifest.js +0 -11
- package/dist/env-manifest.js.map +1 -1
- package/dist/events/index.d.ts +0 -51
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +0 -42
- package/dist/events/index.js.map +1 -1
- package/dist/events/scheduler.d.ts +0 -8
- package/dist/events/scheduler.d.ts.map +1 -1
- package/dist/events/scheduler.js +0 -8
- package/dist/events/scheduler.js.map +1 -1
- package/dist/file-guards.d.ts +0 -15
- package/dist/file-guards.d.ts.map +1 -1
- package/dist/file-guards.js +2 -26
- package/dist/file-guards.js.map +1 -1
- package/dist/harness/execution-env.d.ts +0 -15
- package/dist/harness/execution-env.d.ts.map +1 -1
- package/dist/harness/execution-env.js +4 -55
- package/dist/harness/execution-env.js.map +1 -1
- package/dist/harness/http.d.ts +0 -12
- package/dist/harness/http.d.ts.map +1 -1
- package/dist/harness/http.js +0 -34
- package/dist/harness/http.js.map +1 -1
- package/dist/harness/index.d.ts +4 -7
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +4 -7
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/jev.d.ts +0 -28
- package/dist/harness/jev.d.ts.map +1 -1
- package/dist/harness/jev.js +0 -45
- package/dist/harness/jev.js.map +1 -1
- package/dist/harness/mcp-result.d.ts +20 -0
- package/dist/harness/mcp-result.d.ts.map +1 -0
- package/dist/harness/mcp-result.js +171 -0
- package/dist/harness/mcp-result.js.map +1 -0
- package/dist/harness/mcp.d.ts +0 -19
- package/dist/harness/mcp.d.ts.map +1 -1
- package/dist/harness/mcp.js +7 -66
- package/dist/harness/mcp.js.map +1 -1
- package/dist/harness/models.d.ts +0 -19
- package/dist/harness/models.d.ts.map +1 -1
- package/dist/harness/models.js +0 -52
- package/dist/harness/models.js.map +1 -1
- package/dist/harness/open-connector.d.ts +0 -12
- package/dist/harness/open-connector.d.ts.map +1 -1
- package/dist/harness/open-connector.js +0 -22
- package/dist/harness/open-connector.js.map +1 -1
- package/dist/harness/presenter.d.ts +1 -0
- package/dist/harness/presenter.d.ts.map +1 -1
- package/dist/harness/presenter.js +16 -66
- package/dist/harness/presenter.js.map +1 -1
- package/dist/harness/prompt.d.ts +0 -11
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +1 -18
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/runner.d.ts.map +1 -1
- package/dist/harness/runner.js +8 -28
- package/dist/harness/runner.js.map +1 -1
- package/dist/harness/session.d.ts +11 -28
- package/dist/harness/session.d.ts.map +1 -1
- package/dist/harness/session.js +100 -38
- package/dist/harness/session.js.map +1 -1
- package/dist/harness/skills.d.ts +0 -22
- package/dist/harness/skills.d.ts.map +1 -1
- package/dist/harness/skills.js +0 -20
- package/dist/harness/skills.js.map +1 -1
- package/dist/harness/subagent-profiles.d.ts +0 -4
- package/dist/harness/subagent-profiles.d.ts.map +1 -1
- package/dist/harness/subagent-profiles.js +0 -29
- package/dist/harness/subagent-profiles.js.map +1 -1
- package/dist/harness/subagent.d.ts +0 -25
- package/dist/harness/subagent.d.ts.map +1 -1
- package/dist/harness/subagent.js +1 -70
- package/dist/harness/subagent.js.map +1 -1
- package/dist/harness/tools/event.d.ts.map +1 -1
- package/dist/harness/tools/event.js +0 -9
- package/dist/harness/tools/event.js.map +1 -1
- package/dist/harness/tools/generate-image.d.ts +0 -6
- package/dist/harness/tools/generate-image.d.ts.map +1 -1
- package/dist/harness/tools/generate-image.js.map +1 -1
- package/dist/harness/tools/host-fn-tool.d.ts +0 -17
- package/dist/harness/tools/host-fn-tool.d.ts.map +1 -1
- package/dist/harness/tools/host-fn-tool.js +0 -16
- package/dist/harness/tools/host-fn-tool.js.map +1 -1
- package/dist/harness/tools/index.d.ts +1 -7
- package/dist/harness/tools/index.d.ts.map +1 -1
- package/dist/harness/tools/index.js +3 -14
- package/dist/harness/tools/index.js.map +1 -1
- package/dist/harness/tools/jev-browser.d.ts +4 -94
- package/dist/harness/tools/jev-browser.d.ts.map +1 -1
- package/dist/harness/tools/jev-browser.js +203 -92
- package/dist/harness/tools/jev-browser.js.map +1 -1
- package/dist/harness/tools/jev.d.ts +0 -8
- package/dist/harness/tools/jev.d.ts.map +1 -1
- package/dist/harness/tools/jev.js +0 -11
- package/dist/harness/tools/jev.js.map +1 -1
- package/dist/harness/tools/pi-tools.d.ts +1 -11
- package/dist/harness/tools/pi-tools.d.ts.map +1 -1
- package/dist/harness/tools/pi-tools.js +1 -30
- package/dist/harness/tools/pi-tools.js.map +1 -1
- package/dist/harness/tools/react.d.ts +0 -5
- package/dist/harness/tools/react.d.ts.map +1 -1
- package/dist/harness/tools/react.js +0 -5
- package/dist/harness/tools/react.js.map +1 -1
- package/dist/harness/tools/secret-redaction.d.ts +0 -25
- package/dist/harness/tools/secret-redaction.d.ts.map +1 -1
- package/dist/harness/tools/secret-redaction.js +3 -36
- package/dist/harness/tools/secret-redaction.js.map +1 -1
- package/dist/harness/tools/subagent.d.ts +0 -34
- package/dist/harness/tools/subagent.d.ts.map +1 -1
- package/dist/harness/tools/subagent.js +0 -92
- package/dist/harness/tools/subagent.js.map +1 -1
- package/dist/harness/tools/task.d.ts +0 -1
- package/dist/harness/tools/task.d.ts.map +1 -1
- package/dist/harness/tools/task.js +0 -1
- package/dist/harness/tools/task.js.map +1 -1
- package/dist/harness/tools/types.d.ts +0 -16
- package/dist/harness/tools/types.d.ts.map +1 -1
- package/dist/harness/tools/types.js.map +1 -1
- package/dist/harness/types.d.ts +1 -77
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +0 -13
- package/dist/log.js.map +1 -1
- package/dist/main.js +7 -52
- package/dist/main.js.map +1 -1
- package/dist/memory-capture/index.d.ts +16 -0
- package/dist/memory-capture/index.d.ts.map +1 -0
- package/dist/memory-capture/index.js +194 -0
- package/dist/memory-capture/index.js.map +1 -0
- package/dist/memory-capture/types.d.ts +27 -0
- package/dist/memory-capture/types.d.ts.map +1 -0
- package/dist/memory-capture/types.js.map +1 -0
- package/dist/observability/index.d.ts +0 -9
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +0 -9
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/instrument.js +0 -5
- package/dist/observability/instrument.js.map +1 -1
- package/dist/observability/otel.js +0 -1
- package/dist/observability/otel.js.map +1 -1
- package/dist/observability/sentry.d.ts +0 -18
- package/dist/observability/sentry.d.ts.map +1 -1
- package/dist/observability/sentry.js +0 -36
- package/dist/observability/sentry.js.map +1 -1
- package/dist/observability/types.d.ts +1 -10
- package/dist/observability/types.d.ts.map +1 -1
- package/dist/observability/types.js.map +1 -1
- package/dist/office/index.d.ts +0 -76
- package/dist/office/index.d.ts.map +1 -1
- package/dist/office/index.js +4 -154
- package/dist/office/index.js.map +1 -1
- package/dist/office/projection.d.ts +0 -29
- package/dist/office/projection.d.ts.map +1 -1
- package/dist/office/projection.js +5 -41
- package/dist/office/projection.js.map +1 -1
- package/dist/office/types.d.ts +0 -66
- package/dist/office/types.d.ts.map +1 -1
- package/dist/office/types.js.map +1 -1
- package/dist/runtime/conversation-runtime.d.ts.map +1 -1
- package/dist/runtime/conversation-runtime.js +22 -83
- package/dist/runtime/conversation-runtime.js.map +1 -1
- package/dist/runtime/session-lifecycle.d.ts +0 -10
- package/dist/runtime/session-lifecycle.d.ts.map +1 -1
- package/dist/runtime/session-lifecycle.js +0 -12
- package/dist/runtime/session-lifecycle.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -29
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/sandbox/cloudflare.d.ts.map +1 -1
- package/dist/sandbox/cloudflare.js +2 -1
- package/dist/sandbox/cloudflare.js.map +1 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +21 -15
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +0 -2
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sandbox/identity.d.ts +0 -28
- package/dist/sandbox/identity.d.ts.map +1 -1
- package/dist/sandbox/identity.js +0 -28
- package/dist/sandbox/identity.js.map +1 -1
- package/dist/sandbox/index.d.ts +0 -18
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +2 -19
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/layout.d.ts +7 -0
- package/dist/sandbox/layout.d.ts.map +1 -0
- package/dist/sandbox/layout.js +13 -0
- package/dist/sandbox/layout.js.map +1 -0
- package/dist/sandbox/provisioner.d.ts +24 -65
- package/dist/sandbox/provisioner.d.ts.map +1 -1
- package/dist/sandbox/provisioner.js +213 -125
- package/dist/sandbox/provisioner.js.map +1 -1
- package/dist/sandbox/types.d.ts +0 -39
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sandbox/utils.d.ts +2 -5
- package/dist/sandbox/utils.d.ts.map +1 -1
- package/dist/sandbox/utils.js +31 -31
- package/dist/sandbox/utils.js.map +1 -1
- package/dist/sessions/chat-history-sync.d.ts +0 -6
- package/dist/sessions/chat-history-sync.d.ts.map +1 -1
- package/dist/sessions/chat-history-sync.js +1 -33
- package/dist/sessions/chat-history-sync.js.map +1 -1
- package/dist/sessions/history-line.d.ts +0 -20
- package/dist/sessions/history-line.d.ts.map +1 -1
- package/dist/sessions/history-line.js +0 -20
- package/dist/sessions/history-line.js.map +1 -1
- package/dist/sessions/migrate-common.d.ts +0 -15
- package/dist/sessions/migrate-common.d.ts.map +1 -1
- package/dist/sessions/migrate-common.js +0 -19
- package/dist/sessions/migrate-common.js.map +1 -1
- package/dist/sessions/migrate-pi-084.d.ts.map +1 -1
- package/dist/sessions/migrate-pi-084.js +0 -7
- package/dist/sessions/migrate-pi-084.js.map +1 -1
- package/dist/sessions/migrate-v3.d.ts +0 -7
- package/dist/sessions/migrate-v3.d.ts.map +1 -1
- package/dist/sessions/migrate-v3.js +1 -67
- package/dist/sessions/migrate-v3.js.map +1 -1
- package/dist/sessions/session-key.d.ts +3 -17
- package/dist/sessions/session-key.d.ts.map +1 -1
- package/dist/sessions/session-key.js +8 -35
- package/dist/sessions/session-key.js.map +1 -1
- package/dist/sessions/session-store.d.ts +1 -7
- package/dist/sessions/session-store.d.ts.map +1 -1
- package/dist/sessions/session-store.js +8 -50
- package/dist/sessions/session-store.js.map +1 -1
- package/dist/sessions/store.d.ts +0 -60
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +0 -113
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +0 -17
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/settings/apply.d.ts +0 -6
- package/dist/settings/apply.d.ts.map +1 -1
- package/dist/settings/apply.js +0 -29
- package/dist/settings/apply.js.map +1 -1
- package/dist/settings/index.d.ts +0 -53
- package/dist/settings/index.d.ts.map +1 -1
- package/dist/settings/index.js +0 -77
- package/dist/settings/index.js.map +1 -1
- package/dist/settings/migrate.d.ts +0 -5
- package/dist/settings/migrate.d.ts.map +1 -1
- package/dist/settings/migrate.js +0 -17
- package/dist/settings/migrate.js.map +1 -1
- package/dist/types.d.ts +9 -159
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vault/index.d.ts +2 -36
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +135 -65
- package/dist/vault/index.js.map +1 -1
- package/dist/vault/types.d.ts +0 -15
- package/dist/vault/types.d.ts.map +1 -1
- package/dist/vault/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/dream/index.d.ts +0 -20
- package/dist/dream/index.d.ts.map +0 -1
- package/dist/dream/index.js +0 -264
- package/dist/dream/index.js.map +0 -1
- package/dist/dream/types.d.ts +0 -30
- package/dist/dream/types.d.ts.map +0 -1
- package/dist/dream/types.js.map +0 -1
- /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
|
-
{'&':'&','<':'<','>':'>','"':'"',"'":'''}[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
|
-
${
|
|
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
|