botmux 2.107.0 → 2.108.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +17 -14
- package/README.md +19 -14
- package/dist/adapters/backend/sandbox.d.ts +3 -1
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +19 -3
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +16 -1
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +24 -15
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +75 -43
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/cli/genius.d.ts.map +1 -1
- package/dist/adapters/cli/genius.js +9 -15
- package/dist/adapters/cli/genius.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts +73 -0
- package/dist/adapters/cli/grok.d.ts.map +1 -0
- package/dist/adapters/cli/grok.js +298 -0
- package/dist/adapters/cli/grok.js.map +1 -0
- package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
- package/dist/adapters/cli/oh-my-pi.js +133 -12
- package/dist/adapters/cli/oh-my-pi.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +2 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +3 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +8 -4
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +8 -4
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +18 -16
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/adapters/hook-installer.d.ts +5 -4
- package/dist/adapters/hook-installer.d.ts.map +1 -1
- package/dist/adapters/hook-installer.js +42 -0
- package/dist/adapters/hook-installer.js.map +1 -1
- package/dist/cli/send-dispatch.d.ts +21 -0
- package/dist/cli/send-dispatch.d.ts.map +1 -1
- package/dist/cli/send-dispatch.js +159 -0
- package/dist/cli/send-dispatch.js.map +1 -1
- package/dist/cli/session-list-liveness.d.ts +22 -0
- package/dist/cli/session-list-liveness.d.ts.map +1 -0
- package/dist/cli/session-list-liveness.js +22 -0
- package/dist/cli/session-list-liveness.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +182 -33
- package/dist/cli.js.map +1 -1
- package/dist/core/ask-types.d.ts +4 -0
- package/dist/core/ask-types.d.ts.map +1 -1
- package/dist/core/ask-types.js +10 -0
- package/dist/core/ask-types.js.map +1 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +35 -13
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +89 -16
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +6 -2
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dashboard-url.d.ts +6 -5
- package/dist/core/dashboard-url.d.ts.map +1 -1
- package/dist/core/dashboard-url.js +11 -8
- package/dist/core/dashboard-url.js.map +1 -1
- package/dist/core/maintenance.d.ts +11 -8
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +45 -20
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/per-bot-env.d.ts.map +1 -1
- package/dist/core/per-bot-env.js +4 -0
- package/dist/core/per-bot-env.js.map +1 -1
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +4 -0
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/skills/cli-admin-command.d.ts.map +1 -1
- package/dist/core/skills/cli-admin-command.js +8 -2
- package/dist/core/skills/cli-admin-command.js.map +1 -1
- package/dist/core/skills/im-command.d.ts.map +1 -1
- package/dist/core/skills/im-command.js +3 -1
- package/dist/core/skills/im-command.js.map +1 -1
- package/dist/core/skills/sources.d.ts +33 -1
- package/dist/core/skills/sources.d.ts.map +1 -1
- package/dist/core/skills/sources.js +97 -0
- package/dist/core/skills/sources.js.map +1 -1
- package/dist/core/skills/types.d.ts +8 -0
- package/dist/core/skills/types.d.ts.map +1 -1
- package/dist/core/terminal-url.d.ts.map +1 -1
- package/dist/core/terminal-url.js +12 -2
- package/dist/core/terminal-url.js.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +34 -0
- package/dist/core/terminal-write-auth.d.ts.map +1 -0
- package/dist/core/terminal-write-auth.js +76 -0
- package/dist/core/terminal-write-auth.js.map +1 -0
- package/dist/core/tmux-recovery.d.ts +11 -0
- package/dist/core/tmux-recovery.d.ts.map +1 -0
- package/dist/core/tmux-recovery.js +19 -0
- package/dist/core/tmux-recovery.js.map +1 -0
- package/dist/core/types.d.ts +10 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/update-check.d.ts +2 -2
- package/dist/core/update-check.js +2 -2
- package/dist/core/worker-pool.d.ts +3 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +70 -3
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +15 -8
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/auth.d.ts +1 -1
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +7 -2
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +3 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +3 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/connector-api.d.ts.map +1 -1
- package/dist/dashboard/connector-api.js +39 -13
- package/dist/dashboard/connector-api.js.map +1 -1
- package/dist/dashboard/history-senders.d.ts +37 -0
- package/dist/dashboard/history-senders.d.ts.map +1 -0
- package/dist/dashboard/history-senders.js +60 -0
- package/dist/dashboard/history-senders.js.map +1 -0
- package/dist/dashboard/roles-batch.d.ts +31 -0
- package/dist/dashboard/roles-batch.d.ts.map +1 -0
- package/dist/dashboard/roles-batch.js +87 -0
- package/dist/dashboard/roles-batch.js.map +1 -0
- package/dist/dashboard/settings-card-model.d.ts +4 -2
- package/dist/dashboard/settings-card-model.d.ts.map +1 -1
- package/dist/dashboard/settings-card-model.js +13 -5
- package/dist/dashboard/settings-card-model.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +4 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +6 -1
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/skill-install-request.d.ts +4 -0
- package/dist/dashboard/skill-install-request.d.ts.map +1 -1
- package/dist/dashboard/skill-install-request.js +11 -1
- package/dist/dashboard/skill-install-request.js.map +1 -1
- package/dist/dashboard/trigger-api.d.ts +9 -1
- package/dist/dashboard/trigger-api.d.ts.map +1 -1
- package/dist/dashboard/trigger-api.js +48 -10
- package/dist/dashboard/trigger-api.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +286 -271
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +26 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +1408 -12
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +91 -4
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +43 -2141
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts +6 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +261 -306
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/connectors-page.d.ts +4 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +228 -48
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/dashboard-api.d.ts +9 -0
- package/dist/dashboard/web/dashboard-api.d.ts.map +1 -0
- package/dist/dashboard/web/dashboard-api.js +18 -0
- package/dist/dashboard/web/dashboard-api.js.map +1 -0
- package/dist/dashboard/web/dashboard-components.d.ts +92 -0
- package/dist/dashboard/web/dashboard-components.d.ts.map +1 -0
- package/dist/dashboard/web/dashboard-components.js +250 -0
- package/dist/dashboard/web/dashboard-components.js.map +1 -0
- package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-routes.js +30 -183
- package/dist/dashboard/web/dashboard-routes.js.map +1 -1
- package/dist/dashboard/web/floating-scrollbars.d.ts +2 -0
- package/dist/dashboard/web/floating-scrollbars.d.ts.map +1 -0
- package/dist/dashboard/web/floating-scrollbars.js +264 -0
- package/dist/dashboard/web/floating-scrollbars.js.map +1 -0
- package/dist/dashboard/web/groups-api.d.ts +36 -0
- package/dist/dashboard/web/groups-api.d.ts.map +1 -0
- package/dist/dashboard/web/groups-api.js +44 -0
- package/dist/dashboard/web/groups-api.js.map +1 -0
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +746 -15
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/groups.d.ts +51 -18
- package/dist/dashboard/web/groups.d.ts.map +1 -1
- package/dist/dashboard/web/groups.js +145 -1096
- package/dist/dashboard/web/groups.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +545 -255
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/insights-page.d.ts.map +1 -1
- package/dist/dashboard/web/insights-page.js +1047 -14
- package/dist/dashboard/web/insights-page.js.map +1 -1
- package/dist/dashboard/web/insights.d.ts +177 -7
- package/dist/dashboard/web/insights.d.ts.map +1 -1
- package/dist/dashboard/web/insights.js +187 -1926
- package/dist/dashboard/web/insights.js.map +1 -1
- package/dist/dashboard/web/legacy-workflow-page.d.ts +3 -0
- package/dist/dashboard/web/legacy-workflow-page.d.ts.map +1 -0
- package/dist/dashboard/web/legacy-workflow-page.js +717 -0
- package/dist/dashboard/web/legacy-workflow-page.js.map +1 -0
- package/dist/dashboard/web/monitor-room.d.ts.map +1 -1
- package/dist/dashboard/web/monitor-room.js +10 -8
- package/dist/dashboard/web/monitor-room.js.map +1 -1
- package/dist/dashboard/web/monitoring-page.d.ts.map +1 -1
- package/dist/dashboard/web/monitoring-page.js +188 -14
- package/dist/dashboard/web/monitoring-page.js.map +1 -1
- package/dist/dashboard/web/office-page.d.ts.map +1 -1
- package/dist/dashboard/web/office-page.js +11 -23
- package/dist/dashboard/web/office-page.js.map +1 -1
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js +141 -147
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/overview.d.ts +0 -1
- package/dist/dashboard/web/overview.d.ts.map +1 -1
- package/dist/dashboard/web/overview.js +3 -275
- package/dist/dashboard/web/overview.js.map +1 -1
- package/dist/dashboard/web/preferences.d.ts +1 -6
- package/dist/dashboard/web/preferences.d.ts.map +1 -1
- package/dist/dashboard/web/preferences.js +0 -21
- package/dist/dashboard/web/preferences.js.map +1 -1
- package/dist/dashboard/web/role-batch.d.ts +9 -0
- package/dist/dashboard/web/role-batch.d.ts.map +1 -0
- package/dist/dashboard/web/role-batch.js +42 -0
- package/dist/dashboard/web/role-batch.js.map +1 -0
- package/dist/dashboard/web/roles-page.d.ts.map +1 -1
- package/dist/dashboard/web/roles-page.js +538 -13
- package/dist/dashboard/web/roles-page.js.map +1 -1
- package/dist/dashboard/web/roles.d.ts +98 -1
- package/dist/dashboard/web/roles.d.ts.map +1 -1
- package/dist/dashboard/web/roles.js +117 -903
- package/dist/dashboard/web/roles.js.map +1 -1
- package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
- package/dist/dashboard/web/schedules-page.js +82 -24
- package/dist/dashboard/web/schedules-page.js.map +1 -1
- package/dist/dashboard/web/sessions-kanban.d.ts +67 -0
- package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -0
- package/dist/dashboard/web/sessions-kanban.js +481 -0
- package/dist/dashboard/web/sessions-kanban.js.map +1 -0
- package/dist/dashboard/web/sessions-page.d.ts +4 -0
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +1633 -18
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +45 -4
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +53 -2535
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +0 -2
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +127 -117
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/skills-page.d.ts +10 -1
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +235 -93
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/web/team-federation-page.d.ts.map +1 -1
- package/dist/dashboard/web/team-federation-page.js +446 -28
- package/dist/dashboard/web/team-federation-page.js.map +1 -1
- package/dist/dashboard/web/team-federation.d.ts +91 -2
- package/dist/dashboard/web/team-federation.d.ts.map +1 -1
- package/dist/dashboard/web/team-federation.js +87 -474
- package/dist/dashboard/web/team-federation.js.map +1 -1
- package/dist/dashboard/web/theme-menu.d.ts +1 -0
- package/dist/dashboard/web/theme-menu.d.ts.map +1 -1
- package/dist/dashboard/web/theme-menu.js +79 -20
- package/dist/dashboard/web/theme-menu.js.map +1 -1
- package/dist/dashboard/web/ui.d.ts +1 -2
- package/dist/dashboard/web/ui.d.ts.map +1 -1
- package/dist/dashboard/web/ui.js +8 -20
- package/dist/dashboard/web/ui.js.map +1 -1
- package/dist/dashboard/web/v3-api.d.ts +16 -0
- package/dist/dashboard/web/v3-api.d.ts.map +1 -0
- package/dist/dashboard/web/v3-api.js +21 -0
- package/dist/dashboard/web/v3-api.js.map +1 -0
- package/dist/dashboard/web/v3-components.d.ts +2 -0
- package/dist/dashboard/web/v3-components.d.ts.map +1 -0
- package/dist/dashboard/web/v3-components.js +290 -0
- package/dist/dashboard/web/v3-components.js.map +1 -0
- package/dist/dashboard/web/v3-model.d.ts +80 -0
- package/dist/dashboard/web/v3-model.d.ts.map +1 -0
- package/dist/dashboard/web/v3-model.js +240 -0
- package/dist/dashboard/web/v3-model.js.map +1 -0
- package/dist/dashboard/web/v3-page.d.ts.map +1 -1
- package/dist/dashboard/web/v3-page.js +2 -22
- package/dist/dashboard/web/v3-page.js.map +1 -1
- package/dist/dashboard/web/v3-terminal.d.ts +14 -0
- package/dist/dashboard/web/v3-terminal.d.ts.map +1 -1
- package/dist/dashboard/web/v3-terminal.js +31 -19
- package/dist/dashboard/web/v3-terminal.js.map +1 -1
- package/dist/dashboard/web/webhook-logs-page.d.ts +16 -0
- package/dist/dashboard/web/webhook-logs-page.d.ts.map +1 -0
- package/dist/dashboard/web/webhook-logs-page.js +195 -0
- package/dist/dashboard/web/webhook-logs-page.js.map +1 -0
- package/dist/dashboard/web/whiteboards-page.d.ts.map +1 -1
- package/dist/dashboard/web/whiteboards-page.js +18 -23
- package/dist/dashboard/web/whiteboards-page.js.map +1 -1
- package/dist/dashboard/web/workflow-version-switch.d.ts +8 -0
- package/dist/dashboard/web/workflow-version-switch.d.ts.map +1 -0
- package/dist/dashboard/web/workflow-version-switch.js +9 -0
- package/dist/dashboard/web/workflow-version-switch.js.map +1 -0
- package/dist/dashboard/web/workflows.d.ts +272 -1
- package/dist/dashboard/web/workflows.d.ts.map +1 -1
- package/dist/dashboard/web/workflows.js +211 -1397
- package/dist/dashboard/web/workflows.js.map +1 -1
- package/dist/dashboard/webhook-routes.d.ts.map +1 -1
- package/dist/dashboard/webhook-routes.js +92 -39
- package/dist/dashboard/webhook-routes.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -65
- package/dist/dashboard-web/brand-logo.png +0 -0
- package/dist/dashboard-web/chunks/bot-defaults-page-HSQ6HR3R.js +1 -0
- package/dist/dashboard-web/chunks/chunk-6TPODDDC.js +1 -0
- package/dist/dashboard-web/chunks/chunk-73EJP2MO.js +1 -0
- package/dist/dashboard-web/chunks/chunk-BRTZFELS.js +1 -0
- package/dist/dashboard-web/chunks/chunk-EVK77ASG.js +1 -0
- package/dist/dashboard-web/chunks/chunk-R3FXBFQH.js +1 -0
- package/dist/dashboard-web/chunks/chunk-VRLL7SNU.js +1 -0
- package/dist/dashboard-web/chunks/chunk-XCPSHD3G.js +37 -0
- package/dist/dashboard-web/chunks/chunk-XJ7BGPJH.js +1 -0
- package/dist/dashboard-web/chunks/chunk-YVRWYGXR.js +1 -0
- package/dist/dashboard-web/chunks/chunk-ZJING6FY.js +66 -0
- package/dist/dashboard-web/chunks/connectors-page-ODUA5UCW.js +1 -0
- package/dist/dashboard-web/chunks/groups-page-OYX5M6NK.js +8 -0
- package/dist/dashboard-web/chunks/insights-page-YCQO6OCR.js +22 -0
- package/dist/dashboard-web/chunks/legacy-workflow-page-XJAUNONP.js +2 -0
- package/dist/dashboard-web/chunks/{monitor-room-RFX4HPS5.js → monitor-room-WTZNYAFV.js} +17 -15
- package/dist/dashboard-web/chunks/monitoring-page-R6LJUK7G.js +2 -0
- package/dist/dashboard-web/chunks/office-page-4OXK3GNR.js +1 -0
- package/dist/dashboard-web/chunks/overview-page-4MVEJY6U.js +1 -0
- package/dist/dashboard-web/chunks/roles-page-NQDW27VV.js +1 -0
- package/dist/dashboard-web/chunks/schedules-page-JRNKYHOX.js +1 -0
- package/dist/dashboard-web/chunks/sessions-page-LF7UVI7B.js +3 -0
- package/dist/dashboard-web/chunks/settings-page-WGW7KQQS.js +2 -0
- package/dist/dashboard-web/chunks/skills-page-AMD4XP3T.js +3 -0
- package/dist/dashboard-web/chunks/team-federation-page-SYG3K4GG.js +1 -0
- package/dist/dashboard-web/chunks/v3-page-6NFFWNUK.js +94 -0
- package/dist/dashboard-web/chunks/whiteboards-page-FBRUK2AS.js +1 -0
- package/dist/dashboard-web/favicon.png +0 -0
- package/dist/dashboard-web/index.html +5 -69
- package/dist/dashboard-web/style.css +18835 -7211
- package/dist/dashboard.js +150 -32
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +2 -2
- package/dist/global-config.d.ts.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +3 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +3 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +7 -5
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +10 -7
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +25 -1
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +84 -0
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/platform/binding.d.ts +9 -0
- package/dist/platform/binding.d.ts.map +1 -1
- package/dist/platform/binding.js +14 -0
- package/dist/platform/binding.js.map +1 -1
- package/dist/platform/tunnel-client.d.ts.map +1 -1
- package/dist/platform/tunnel-client.js +9 -0
- package/dist/platform/tunnel-client.js.map +1 -1
- package/dist/services/bot-config-store.d.ts +1 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +11 -5
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/file-bridge-path.d.ts +8 -0
- package/dist/services/file-bridge-path.d.ts.map +1 -0
- package/dist/services/file-bridge-path.js +75 -0
- package/dist/services/file-bridge-path.js.map +1 -0
- package/dist/services/grok-paths.d.ts +27 -0
- package/dist/services/grok-paths.d.ts.map +1 -0
- package/dist/services/grok-paths.js +106 -0
- package/dist/services/grok-paths.js.map +1 -0
- package/dist/services/grok-transcript.d.ts +67 -0
- package/dist/services/grok-transcript.d.ts.map +1 -0
- package/dist/services/grok-transcript.js +630 -0
- package/dist/services/grok-transcript.js.map +1 -0
- package/dist/services/hermes-session-filter.d.ts +28 -0
- package/dist/services/hermes-session-filter.d.ts.map +1 -0
- package/dist/services/hermes-session-filter.js +53 -0
- package/dist/services/hermes-session-filter.js.map +1 -0
- package/dist/services/hermes-transcript.d.ts +3 -0
- package/dist/services/hermes-transcript.d.ts.map +1 -1
- package/dist/services/hermes-transcript.js +20 -2
- package/dist/services/hermes-transcript.js.map +1 -1
- package/dist/services/resumable-session-discovery.d.ts +3 -0
- package/dist/services/resumable-session-discovery.d.ts.map +1 -1
- package/dist/services/resumable-session-discovery.js +6 -1
- package/dist/services/resumable-session-discovery.js.map +1 -1
- package/dist/services/skill-registry-store.d.ts +9 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +304 -1
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/structured-bridge-clis.d.ts +29 -0
- package/dist/services/structured-bridge-clis.d.ts.map +1 -0
- package/dist/services/structured-bridge-clis.js +76 -0
- package/dist/services/structured-bridge-clis.js.map +1 -0
- package/dist/services/trigger-log-store.d.ts +59 -0
- package/dist/services/trigger-log-store.d.ts.map +1 -1
- package/dist/services/trigger-log-store.js +81 -10
- package/dist/services/trigger-log-store.js.map +1 -1
- package/dist/services/usage-ledger.d.ts +4 -1
- package/dist/services/usage-ledger.d.ts.map +1 -1
- package/dist/services/usage-ledger.js +48 -10
- package/dist/services/usage-ledger.js.map +1 -1
- package/dist/services/webhook-audit.d.ts +10 -0
- package/dist/services/webhook-audit.d.ts.map +1 -0
- package/dist/services/webhook-audit.js +116 -0
- package/dist/services/webhook-audit.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +2 -0
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-tmux.d.ts +26 -12
- package/dist/setup/ensure-tmux.d.ts.map +1 -1
- package/dist/setup/ensure-tmux.js +85 -18
- package/dist/setup/ensure-tmux.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +14 -1
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/injection-mode.d.ts +16 -0
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +25 -0
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/global-install.d.ts +22 -0
- package/dist/utils/global-install.d.ts.map +1 -0
- package/dist/utils/global-install.js +106 -0
- package/dist/utils/global-install.js.map +1 -0
- package/dist/utils/install-diagnostics.d.ts +1 -1
- package/dist/utils/install-diagnostics.d.ts.map +1 -1
- package/dist/utils/install-diagnostics.js +6 -4
- package/dist/utils/install-diagnostics.js.map +1 -1
- package/dist/utils/install-info.d.ts +2 -0
- package/dist/utils/install-info.d.ts.map +1 -1
- package/dist/utils/install-info.js +10 -4
- package/dist/utils/install-info.js.map +1 -1
- package/dist/worker.js +293 -172
- package/dist/worker.js.map +1 -1
- package/dist/workflows/events/payloads.d.ts +10 -10
- package/dist/workflows/events/schema.d.ts +28 -28
- package/package.json +8 -7
- package/dist/dashboard/web/connectors.d.ts +0 -2
- package/dist/dashboard/web/connectors.d.ts.map +0 -1
- package/dist/dashboard/web/connectors.js +0 -2
- package/dist/dashboard/web/connectors.js.map +0 -1
- package/dist/dashboard/web/office.d.ts +0 -2
- package/dist/dashboard/web/office.d.ts.map +0 -1
- package/dist/dashboard/web/office.js +0 -2
- package/dist/dashboard/web/office.js.map +0 -1
- package/dist/dashboard/web/schedules.d.ts +0 -2
- package/dist/dashboard/web/schedules.d.ts.map +0 -1
- package/dist/dashboard/web/schedules.js +0 -2
- package/dist/dashboard/web/schedules.js.map +0 -1
- package/dist/dashboard/web/settings.d.ts +0 -2
- package/dist/dashboard/web/settings.d.ts.map +0 -1
- package/dist/dashboard/web/settings.js +0 -2
- package/dist/dashboard/web/settings.js.map +0 -1
- package/dist/dashboard/web/skills.d.ts +0 -2
- package/dist/dashboard/web/skills.d.ts.map +0 -1
- package/dist/dashboard/web/skills.js +0 -2
- package/dist/dashboard/web/skills.js.map +0 -1
- package/dist/dashboard/web/skin-intro.d.ts +0 -4
- package/dist/dashboard/web/skin-intro.d.ts.map +0 -1
- package/dist/dashboard/web/skin-intro.js +0 -49
- package/dist/dashboard/web/skin-intro.js.map +0 -1
- package/dist/dashboard/web/v3.d.ts +0 -3
- package/dist/dashboard/web/v3.d.ts.map +0 -1
- package/dist/dashboard/web/v3.js +0 -539
- package/dist/dashboard/web/v3.js.map +0 -1
- package/dist/dashboard/web/whiteboards.d.ts +0 -2
- package/dist/dashboard/web/whiteboards.d.ts.map +0 -1
- package/dist/dashboard/web/whiteboards.js +0 -2
- package/dist/dashboard/web/whiteboards.js.map +0 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-OCIHPGEU.js +0 -438
- package/dist/dashboard-web/chunks/chunk-3N2PPQVD.js +0 -1
- package/dist/dashboard-web/chunks/chunk-3UJYRR2Q.js +0 -37
- package/dist/dashboard-web/chunks/chunk-5E3MJJL4.js +0 -1
- package/dist/dashboard-web/chunks/chunk-7SZRL6FY.js +0 -1
- package/dist/dashboard-web/chunks/chunk-JTSE5VP6.js +0 -54
- package/dist/dashboard-web/chunks/chunk-LXIK7OUJ.js +0 -1
- package/dist/dashboard-web/chunks/chunk-PE6W3C2C.js +0 -1
- package/dist/dashboard-web/chunks/chunk-QW54DWU3.js +0 -325
- package/dist/dashboard-web/chunks/connectors-IGETYWCP.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-MYBS4ZYJ.js +0 -207
- package/dist/dashboard-web/chunks/insights-page-UPA32V2S.js +0 -309
- package/dist/dashboard-web/chunks/monitoring-page-7T3BESZ2.js +0 -1
- package/dist/dashboard-web/chunks/office-page-6DXYOG55.js +0 -1
- package/dist/dashboard-web/chunks/overview-page-LJSBCQW4.js +0 -1
- package/dist/dashboard-web/chunks/roles-page-ESURTVQH.js +0 -98
- package/dist/dashboard-web/chunks/schedules-LML2ISOT.js +0 -1
- package/dist/dashboard-web/chunks/sessions-YB73XOTU.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-MCO5QR7Z.js +0 -1
- package/dist/dashboard-web/chunks/settings-page-OSDKNVS3.js +0 -2
- package/dist/dashboard-web/chunks/skills-page-PXBZJ6P6.js +0 -2
- package/dist/dashboard-web/chunks/team-federation-page-TTPVHFPX.js +0 -10
- package/dist/dashboard-web/chunks/v3-page-FVPUL665.js +0 -149
- package/dist/dashboard-web/chunks/whiteboards-page-HLCTYSSV.js +0 -1
- package/dist/dashboard-web/chunks/workflows-FCPM4S4D.js +0 -184
- package/dist/dashboard-web/skins/bluearchive-hero.webp +0 -0
- package/dist/dashboard-web/skins/dragonball-goku.webp +0 -0
- package/dist/dashboard-web/skins/dragonball-wukong.webp +0 -0
- package/dist/dashboard-web/skins/genshin-breeze.webp +0 -0
- package/dist/dashboard-web/skins/ikun-hero.webp +0 -0
- package/dist/dashboard-web/skins/prts-priestess.webp +0 -0
- package/dist/dashboard-web/skins/zzz-hero.webp +0 -0
- package/dist/dashboard-web/skins/zzz-pattern.svg +0 -25
|
@@ -1,64 +1,25 @@
|
|
|
1
|
-
// Sessions page:
|
|
2
|
-
import {
|
|
3
|
-
import { computeDropPosition, deriveKanbanColumn, effectiveKanbanPosition, } from './kanban-model.js';
|
|
4
|
-
import { store } from './store.js';
|
|
5
|
-
import { botDisplayName, botAvatarHtml, chatAvatarHtml, chatDisplayTitle, attentionWaitSince, escapeHtml, loadNameMaps, relTime, stripMentionPrefix, t, ui, } from './ui.js';
|
|
6
|
-
import { IDLE_CLEANUP_HOUR_OPTIONS, parseIdleCleanupHours, selectIdleCleanupCandidates, } from '../session-cleanup.js';
|
|
1
|
+
// Sessions page shared helpers: pure display helpers and small API utilities.
|
|
2
|
+
import { chatDisplayTitle, t, ui, } from './ui.js';
|
|
7
3
|
import { CLI_OPTIONS } from '../../setup/bot-config-editor.js';
|
|
8
|
-
import { addMonitorRoomSessionIds, monitorRoomUrl } from './monitor-room-store.js';
|
|
9
4
|
import { sessionTerminalHref } from './session-terminal.js';
|
|
10
|
-
function
|
|
11
|
-
return `<th data-sort="${sort}" data-label="${escapeHtml(label)}">${escapeHtml(label)}</th>`;
|
|
12
|
-
}
|
|
13
|
-
function tokenCount(value) {
|
|
5
|
+
export function tokenCount(value) {
|
|
14
6
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
15
7
|
}
|
|
16
|
-
function formatTokenCount(value) {
|
|
8
|
+
export function formatTokenCount(value) {
|
|
17
9
|
const n = tokenCount(value);
|
|
18
10
|
return n === null ? '-' : n.toLocaleString('en-US');
|
|
19
11
|
}
|
|
20
12
|
// CLI 过滤选项从 setup 的单一事实源 CLI_OPTIONS 派生,新增 CLI 自动跟随,
|
|
21
13
|
// 不再手抄一份(手抄版曾漏 antigravity/traex/mir/kimi/genius)。
|
|
22
14
|
// 'unknown' 兜底:没有 cliId 的会话在 filtered() 里按 'unknown' 归类。
|
|
23
|
-
const CLI_FILTER_OPTIONS = [...CLI_OPTIONS.map(o => o.id), 'unknown'];
|
|
24
|
-
const BOARD_COLUMNS = [
|
|
15
|
+
export const CLI_FILTER_OPTIONS = [...CLI_OPTIONS.map(o => o.id), 'unknown'];
|
|
16
|
+
export const BOARD_COLUMNS = [
|
|
25
17
|
{ id: 'needs-you', labelKey: 'sessions.board.needsYou', hintKey: 'sessions.board.needsYouHint' },
|
|
26
18
|
{ id: 'starting', labelKey: 'sessions.board.starting', hintKey: 'sessions.board.startingHint' },
|
|
27
19
|
{ id: 'working', labelKey: 'sessions.board.working', hintKey: 'sessions.board.workingHint' },
|
|
28
20
|
{ id: 'idle', labelKey: 'sessions.board.idle', hintKey: 'sessions.board.idleHint' },
|
|
29
21
|
];
|
|
30
|
-
|
|
31
|
-
// 五列手动工作流(待办池/待办/进行中/待确认/已完成):卡片可拖拽换列与列内排序,
|
|
32
|
-
// 放置持久化在 Session 上(daemon /board 端点);未手动放置的会话按运行状态
|
|
33
|
-
// 推导默认列(kanban-model.ts),已关闭会话固定落「已完成」。
|
|
34
|
-
const KANBAN_COLUMNS = [
|
|
35
|
-
{ id: 'backlog', labelKey: 'sessions.kanban.backlog' },
|
|
36
|
-
{ id: 'todo', labelKey: 'sessions.kanban.todo' },
|
|
37
|
-
{ id: 'in_progress', labelKey: 'sessions.kanban.inProgress' },
|
|
38
|
-
{ id: 'in_review', labelKey: 'sessions.kanban.inReview' },
|
|
39
|
-
{ id: 'done', labelKey: 'sessions.kanban.done' },
|
|
40
|
-
];
|
|
41
|
-
// 「已完成」列收纳所有已关闭会话,可能积累上千条——只展示最前的一截,剩余计数提示。
|
|
42
|
-
const KANBAN_DONE_CAP = 50;
|
|
43
|
-
// 列状态图标:14x14 SVG,圆环 + 不同填充度的扇形/对勾表达工作流进度,
|
|
44
|
-
// 颜色由列容器的 currentColor 决定(CSS 里按列着色)。
|
|
45
|
-
function kanbanStatusIcon(id) {
|
|
46
|
-
const ring = (extra = '') => `<svg viewBox="0 0 14 14" aria-hidden="true"><circle cx="7" cy="7" r="5.4" fill="none" stroke="currentColor" stroke-width="1.6"${extra}/>`;
|
|
47
|
-
switch (id) {
|
|
48
|
-
case 'backlog': // 虚线圆环
|
|
49
|
-
return `${ring(' stroke-dasharray="1.6 2.1"')}</svg>`;
|
|
50
|
-
case 'in_progress': // 半圆填充
|
|
51
|
-
return `${ring()}<path d="M7,7 L7,3.6 A3.4,3.4 0 0 1 7,10.4 Z" fill="currentColor"/></svg>`;
|
|
52
|
-
case 'in_review': // 3/4 填充
|
|
53
|
-
return `${ring()}<path d="M7,7 L7,3.6 A3.4,3.4 0 1 1 3.6,7 Z" fill="currentColor"/></svg>`;
|
|
54
|
-
case 'done': // 实心圆 + 对勾
|
|
55
|
-
return `<svg viewBox="0 0 14 14" aria-hidden="true"><circle cx="7" cy="7" r="6.2" fill="currentColor"/><path d="M4.4 7.2 6.2 9 9.7 5.4" fill="none" stroke="var(--surface)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
56
|
-
case 'todo': // 空心圆环
|
|
57
|
-
default:
|
|
58
|
-
return `${ring()}</svg>`;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function cssToken(value) {
|
|
22
|
+
export function cssToken(value) {
|
|
62
23
|
return String(value ?? 'unknown').toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
63
24
|
}
|
|
64
25
|
export const SESSION_STATUS_OPTIONS = [
|
|
@@ -77,11 +38,22 @@ export function sessionStatusText(status) {
|
|
|
77
38
|
const label = t(key);
|
|
78
39
|
return label === key ? raw : label;
|
|
79
40
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
41
|
+
export function sessionRuntimeCounts(rows) {
|
|
42
|
+
let logical = 0;
|
|
43
|
+
let resident = 0;
|
|
44
|
+
let dormant = 0;
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
if (row?.status === 'closed')
|
|
47
|
+
continue;
|
|
48
|
+
logical++;
|
|
49
|
+
if (typeof row?.workerPid === 'number')
|
|
50
|
+
resident++;
|
|
51
|
+
if (row?.status === 'dormant')
|
|
52
|
+
dormant++;
|
|
53
|
+
}
|
|
54
|
+
return { logical, resident, dormant };
|
|
55
|
+
}
|
|
56
|
+
export function repoBasename(workingDir) {
|
|
85
57
|
const value = String(workingDir ?? '').trim();
|
|
86
58
|
if (!value)
|
|
87
59
|
return '-';
|
|
@@ -104,66 +76,41 @@ export function isUnknownChatSession(s, resolveTitle = chatDisplayTitle) {
|
|
|
104
76
|
const chatId = String(s?.chatId ?? '').trim();
|
|
105
77
|
return !!chatId && !resolveTitle(s);
|
|
106
78
|
}
|
|
107
|
-
function sessionLocationTitle(s) {
|
|
79
|
+
export function sessionLocationTitle(s) {
|
|
108
80
|
const label = sessionLocationText(s);
|
|
109
81
|
const chatId = String(s?.chatId ?? '').trim();
|
|
110
82
|
return chatId && !label.includes(chatId) ? `${label} · ${chatId}` : label;
|
|
111
83
|
}
|
|
112
|
-
function sessionSearchText(s) {
|
|
84
|
+
export function sessionSearchText(s) {
|
|
113
85
|
return `${JSON.stringify(s)} ${sessionLocationText(s)} ${sessionLocationTitle(s)}`.toLowerCase();
|
|
114
86
|
}
|
|
115
87
|
export const terminalHref = sessionTerminalHref;
|
|
88
|
+
export function shouldOpenWritableTerminal(state = ui) {
|
|
89
|
+
return state.authed && !state.publicReadOnly;
|
|
90
|
+
}
|
|
116
91
|
// Cohesive icon set for the session-card action bar — stroke-based (CSS sets
|
|
117
92
|
// stroke:currentColor), 16px viewBox to match the sidebar nav glyphs. Icons
|
|
118
|
-
// instead of text labels keep
|
|
119
|
-
|
|
120
|
-
// "Close" onto its own line.
|
|
121
|
-
const ICON = {
|
|
93
|
+
// instead of text labels keep rows fixed width across locales.
|
|
94
|
+
export const ICON = {
|
|
122
95
|
pin: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 14.3s4.2-3.9 4.2-7.3A4.2 4.2 0 0 0 8 2.9a4.2 4.2 0 0 0-4.2 4.1C3.8 10.4 8 14.3 8 14.3z"/><circle cx="8" cy="6.9" r="1.5"/></svg>',
|
|
123
96
|
openChat: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M9.4 2.8h3.8v3.8"/><path d="M13.2 2.8 7.3 8.7"/><path d="M11.5 9.3v2.9a1.2 1.2 0 0 1-1.2 1.2H3.8a1.2 1.2 0 0 1-1.2-1.2V5.7a1.2 1.2 0 0 1 1.2-1.2h2.9"/></svg>',
|
|
124
|
-
details: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2.
|
|
125
|
-
terminal: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="1.
|
|
97
|
+
details: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2.4" y="2.6" width="11.2" height="10.8" rx="2"/><path d="M5.2 5.4h5.6"/><path d="M5.2 8h5.6"/><path d="M5.2 10.6h3.2"/></svg>',
|
|
98
|
+
terminal: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="1.8" y="2.2" width="12.4" height="10.5" rx="2"/><path d="M1.8 5h12.4"/><circle cx="4" cy="3.6" r=".45" fill="currentColor" stroke="none"/><path d="m4.2 7.3 1.8 1.6-1.8 1.6"/><path d="M8 10.6h3.4"/></svg>',
|
|
126
99
|
key: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="6" cy="6.1" r="3"/><path d="M8.1 8.2 13 13.1"/><path d="M11.3 11.4 12.6 10.1"/><path d="M12.7 12.8 13.7 11.8"/></svg>',
|
|
127
|
-
lock: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3
|
|
128
|
-
unlock: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3
|
|
100
|
+
lock: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3" y="7" width="10" height="6.4" rx="1.8"/><path d="M5.1 7V5.4a2.9 2.9 0 0 1 5.8 0V7"/><path d="M8 9.5v1.4"/></svg>',
|
|
101
|
+
unlock: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3" y="7" width="10" height="6.4" rx="1.8"/><path d="M5.1 7V5.3a2.9 2.9 0 0 1 5.1-1.9"/><path d="M8 9.5v1.4"/></svg>',
|
|
129
102
|
close: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4.2 4.2 11.8 11.8"/><path d="M11.8 4.2 4.2 11.8"/></svg>',
|
|
130
103
|
edit: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10.7 3.3 12.7 5.3 6.3 11.7 3.7 12.3 4.3 9.7 10.7 3.3z"/></svg>',
|
|
131
|
-
history: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="
|
|
132
|
-
restart: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M12.
|
|
133
|
-
|
|
134
|
-
// stroke:currentColor 适配本组线性图标,圆角端点呼应原 logo 的圆润羽尖。
|
|
135
|
-
feishu: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.6 4.4C6.4 4 10.4 5.4 13.4 8.2 9.8 7 6.4 6.6 3.4 7.4"/><path d="M13.4 8.2C9.6 8.7 6 10 2.9 12 5.6 9 8.8 7.6 13.4 8.2"/></svg>',
|
|
104
|
+
history: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.2 7.8a4.8 4.8 0 1 0 1.4-3.4"/><path d="M3.2 3.1v3.2h3.2"/><path d="M8 5.4v3l2.1 1.2"/></svg>',
|
|
105
|
+
restart: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M12.7 6.5A4.8 4.8 0 1 0 13 9"/><path d="M12.7 3.3v3.2H9.5"/></svg>',
|
|
106
|
+
feishu: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 5.2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v3.3a2 2 0 0 1-2 2H7.1L4.3 13v-2.5H5"/><path d="M9.1 3.2h3.7v3.7"/><path d="M12.8 3.2 8.5 7.5"/></svg>',
|
|
136
107
|
};
|
|
137
|
-
|
|
138
|
-
function cardActBtn(action, icon, label, kind = '') {
|
|
139
|
-
return `<button type="button" class="card-act${kind ? ' ' + kind : ''}" data-action="${action}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}">${icon}</button>`;
|
|
140
|
-
}
|
|
141
|
-
function lockActionLabel(s) {
|
|
108
|
+
export function lockActionLabel(s) {
|
|
142
109
|
return s.locked ? t('sessions.unlock') : t('sessions.lock');
|
|
143
110
|
}
|
|
144
|
-
function lockChipHtml(s) {
|
|
145
|
-
return s.locked
|
|
146
|
-
? `<span class="session-lock-badge" title="${escapeHtml(t('sessions.locked'))}">${escapeHtml(t('sessions.locked'))}</span>`
|
|
147
|
-
: '';
|
|
148
|
-
}
|
|
149
|
-
// Terminal access pill for a live session: a read-only "open" segment (always
|
|
150
|
-
// shown) plus — for authenticated users — a "key" segment that mints the
|
|
151
|
-
// writable (token-bearing) link on demand. Icon-only + tooltip keeps it compact;
|
|
152
|
-
// the writable segment carries the accent fill so it reads as the real action.
|
|
153
|
-
function terminalControlsHtml(url) {
|
|
154
|
-
if (!url)
|
|
155
|
-
return '';
|
|
156
|
-
const open = `<a class="term-btn term-open" href="${escapeHtml(url)}" target="_blank" rel="noopener" title="${escapeHtml(t('sessions.openTerminal'))}" aria-label="${escapeHtml(t('sessions.openTerminal'))}">${ICON.terminal}</a>`;
|
|
157
|
-
if (!ui.authed)
|
|
158
|
-
return `<span class="term-pill solo">${open}</span>`;
|
|
159
|
-
const write = `<button type="button" class="term-btn term-write" data-action="write-link" title="${escapeHtml(t('sessions.writeLinkHint'))}" aria-label="${escapeHtml(t('sessions.writeLink'))}">${ICON.key}</button>`;
|
|
160
|
-
return `<span class="term-pill">${open}${write}</span>`;
|
|
161
|
-
}
|
|
162
111
|
// Mint + open the writable web terminal for `s`. The tab is opened synchronously
|
|
163
|
-
// inside the click gesture
|
|
164
|
-
|
|
165
|
-
// the new tab's address bar, same as the Lark card's link, by design.
|
|
166
|
-
async function openWriteLink(s, btn) {
|
|
112
|
+
// inside the click gesture so popup blockers do not reject the delayed URL.
|
|
113
|
+
export async function openWriteLink(s, btn) {
|
|
167
114
|
const tab = window.open('about:blank', '_blank');
|
|
168
115
|
if (tab)
|
|
169
116
|
tab.opener = null;
|
|
@@ -174,7 +121,6 @@ async function openWriteLink(s, btn) {
|
|
|
174
121
|
const body = await r.json().catch(() => ({}));
|
|
175
122
|
if (!r.ok || body?.ok === false || !body?.url) {
|
|
176
123
|
tab?.close();
|
|
177
|
-
// 401 already raises the global read-only toast — don't double-alert.
|
|
178
124
|
if (r.status !== 401)
|
|
179
125
|
alert(`${t('sessions.writeLinkFail')}: ${body?.error ?? r.status}`);
|
|
180
126
|
return;
|
|
@@ -193,7 +139,14 @@ async function openWriteLink(s, btn) {
|
|
|
193
139
|
btn.disabled = false;
|
|
194
140
|
}
|
|
195
141
|
}
|
|
196
|
-
function
|
|
142
|
+
export function historySenderKey(message) {
|
|
143
|
+
const rawType = String(message?.senderType ?? 'unknown');
|
|
144
|
+
const type = rawType === 'app' || rawType === 'bot' ? 'bot' : rawType;
|
|
145
|
+
const id = String(message?.senderId ?? '').trim();
|
|
146
|
+
const name = String(message?.senderName ?? '').trim();
|
|
147
|
+
return `${type}:${id || name || 'unknown'}`;
|
|
148
|
+
}
|
|
149
|
+
export function deriveSessionBoardColumn(s) {
|
|
197
150
|
if (s.status === 'closed')
|
|
198
151
|
return null;
|
|
199
152
|
if (s.pendingRepo || s.tuiPromptActive || s.agentAttention || s.status === 'limited')
|
|
@@ -206,21 +159,6 @@ function deriveSessionBoardColumn(s) {
|
|
|
206
159
|
return 'idle';
|
|
207
160
|
return 'idle';
|
|
208
161
|
}
|
|
209
|
-
// CLI 多选收进一个下拉 chip:默认全选时只占一个胶囊位,点开才展开
|
|
210
|
-
// 全部选项,避免 15 个 checkbox 铺满一整行。
|
|
211
|
-
export function renderCliFilterGroup() {
|
|
212
|
-
return `<details class="filter-cli">
|
|
213
|
-
<summary>${t('sessions.cli')} · <b id="cli-filter-count">${t('common.all')}</b></summary>
|
|
214
|
-
<div class="filter-cli-pop" role="group" aria-label="${t('sessions.cli')}">
|
|
215
|
-
${CLI_FILTER_OPTIONS.map(cli => `
|
|
216
|
-
<label class="filter-check">
|
|
217
|
-
<input type="checkbox" name="cli" value="${escapeHtml(cli)}" checked>
|
|
218
|
-
<span>${escapeHtml(cli)}</span>
|
|
219
|
-
</label>
|
|
220
|
-
`).join('')}
|
|
221
|
-
</div>
|
|
222
|
-
</details>`;
|
|
223
|
-
}
|
|
224
162
|
export function restartConfirmMessage(s) {
|
|
225
163
|
const status = String(s.status ?? 'unknown');
|
|
226
164
|
const cli = String(s.cliId ?? 'unknown');
|
|
@@ -237,97 +175,7 @@ export function restartConfirmMessage(s) {
|
|
|
237
175
|
export function canRestartSession(s) {
|
|
238
176
|
return s.status !== 'closed' && !s.adopt && !s.pendingRepo;
|
|
239
177
|
}
|
|
240
|
-
function
|
|
241
|
-
const showUnknownChatsChecked = readStoredSessionsShowUnknownChats(typeof window === 'undefined' ? undefined : window.localStorage) ? ' checked' : '';
|
|
242
|
-
return `<section class="page">
|
|
243
|
-
<div class="page-heading">
|
|
244
|
-
<div>
|
|
245
|
-
<p class="eyebrow">${t('nav.sessions')}</p>
|
|
246
|
-
<h1>${t('sessions.title')}</h1>
|
|
247
|
-
<p>${t('sessions.subtitle')}</p>
|
|
248
|
-
</div>
|
|
249
|
-
<div class="sessions-view-controls">
|
|
250
|
-
<span id="kanban-team-stats" class="kanban-team-stats" hidden></span>
|
|
251
|
-
<select id="kanban-team" class="kanban-team-select" aria-label="${t('sessions.kanban.groupTeam')}" hidden></select>
|
|
252
|
-
<div class="segmented kanban-groupby" id="kanban-groupby" role="group" aria-label="${t('sessions.kanban.groupBy')}" hidden>
|
|
253
|
-
<button type="button" data-groupby="flow">${t('sessions.kanban.groupFlow')}</button>
|
|
254
|
-
<button type="button" data-groupby="team">${t('sessions.kanban.groupTeam')}</button>
|
|
255
|
-
<button type="button" data-groupby="bot">${t('sessions.kanban.groupBot')}</button>
|
|
256
|
-
</div>
|
|
257
|
-
<div class="segmented sessions-view-toggle" role="group" aria-label="${t('sessions.viewMode')}">
|
|
258
|
-
<button type="button" data-view="kanban">${t('sessions.viewKanban')}</button>
|
|
259
|
-
<button type="button" data-view="board">${t('sessions.viewBoard')}</button>
|
|
260
|
-
<button type="button" data-view="table">${t('sessions.viewTable')}</button>
|
|
261
|
-
</div>
|
|
262
|
-
<button type="button" id="monitor-room-open" class="monitor-room-open">${t('sessions.monitorRoom')}</button>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
<form id="filters" class="filters sessions-filters">
|
|
266
|
-
<input type="search" name="q" placeholder="${t('sessions.search')}" />
|
|
267
|
-
<select name="status">
|
|
268
|
-
<option value="">${t('sessions.anyStatus')}</option>
|
|
269
|
-
${SESSION_STATUS_OPTIONS.map(status => `<option value="${escapeHtml(status)}">${escapeHtml(sessionStatusText(status))}</option>`).join('')}
|
|
270
|
-
</select>
|
|
271
|
-
<select name="adopt">
|
|
272
|
-
<option value="">${t('sessions.adoptAny')}</option>
|
|
273
|
-
<option value="yes">${t('sessions.adoptYes')}</option>
|
|
274
|
-
<option value="no">${t('sessions.adoptNo')}</option>
|
|
275
|
-
</select>
|
|
276
|
-
<select name="chat" aria-label="${t('sessions.location')}">
|
|
277
|
-
<option value="">${t('sessions.chatAny')}</option>
|
|
278
|
-
</select>
|
|
279
|
-
${renderCliFilterGroup()}
|
|
280
|
-
<label class="filter-toggle"><input type="checkbox" name="showUnknownChats"${showUnknownChatsChecked}> <span>${t('sessions.showUnknownChats')}</span></label>
|
|
281
|
-
<label class="filter-toggle"><input type="checkbox" name="active" checked> <span>${t('sessions.activeOnly')}</span></label>
|
|
282
|
-
</form>
|
|
283
|
-
<div id="idle-cleanup-bar" class="idle-cleanup-bar">
|
|
284
|
-
<div class="idle-cleanup-summary">
|
|
285
|
-
<span class="idle-cleanup-dot" aria-hidden="true"></span>
|
|
286
|
-
<span id="idle-cleanup-count" class="idle-cleanup-count"></span>
|
|
287
|
-
</div>
|
|
288
|
-
<div class="idle-cleanup-controls">
|
|
289
|
-
<span class="idle-cleanup-label">${t('sessions.idleCleanupOlderThan')}</span>
|
|
290
|
-
<div id="idle-cleanup-threshold" class="idle-cleanup-thresholds" role="group" aria-label="${t('sessions.idleCleanupThreshold')}">
|
|
291
|
-
${IDLE_CLEANUP_HOUR_OPTIONS.map(hours => `<button type="button" data-hours="${hours}" aria-pressed="${hours === 24 ? 'true' : 'false'}">${hours === 168 ? '7d' : `${hours}H`}</button>`).join('')}
|
|
292
|
-
</div>
|
|
293
|
-
<button type="button" id="idle-cleanup-run" class="contrast idle-cleanup-run">${t('sessions.idleCleanupRun')}</button>
|
|
294
|
-
</div>
|
|
295
|
-
<span id="idle-cleanup-status" class="idle-cleanup-status" aria-live="polite"></span>
|
|
296
|
-
</div>
|
|
297
|
-
<div id="bulk-bar" class="bulk-bar" hidden>
|
|
298
|
-
<span id="bulk-count"></span>
|
|
299
|
-
<button type="button" id="bulk-monitor-room">${t('sessions.addToMonitorRoom')}</button>
|
|
300
|
-
<button type="button" id="bulk-lock">${t('sessions.lockSelected')}</button>
|
|
301
|
-
<button type="button" id="bulk-unlock">${t('sessions.unlockSelected')}</button>
|
|
302
|
-
<button type="button" id="bulk-close" class="contrast">${t('sessions.closeSelected')}</button>
|
|
303
|
-
<button type="button" id="bulk-clear">${t('sessions.clearSelection')}</button>
|
|
304
|
-
</div>
|
|
305
|
-
<table id="sessions-table">
|
|
306
|
-
<thead><tr>
|
|
307
|
-
<th><input type="checkbox" id="select-all" title="${t('sessions.activeOnly')}"></th>
|
|
308
|
-
${th('botName', t('sessions.bot'))}
|
|
309
|
-
${th('cliId', t('sessions.cli'))}
|
|
310
|
-
${th('status', t('sessions.status'))}
|
|
311
|
-
${th('chat', t('sessions.location'))}
|
|
312
|
-
${th('tokenIn', t('sessions.tokenIn'))}
|
|
313
|
-
${th('tokenOut', t('sessions.tokenOut'))}
|
|
314
|
-
${th('title', t('sessions.titleCol'))}
|
|
315
|
-
${th('workingDir', t('sessions.workingDir'))}
|
|
316
|
-
${th('spawnedAt', t('sessions.created'))}
|
|
317
|
-
${th('lastMessageAt', t('sessions.last'))}
|
|
318
|
-
${th('adopt', t('sessions.adopt'))}
|
|
319
|
-
<th>${t('sessions.actions')}</th>
|
|
320
|
-
</tr></thead>
|
|
321
|
-
<tbody></tbody>
|
|
322
|
-
</table>
|
|
323
|
-
<div id="sessions-board" class="sessions-board" hidden></div>
|
|
324
|
-
<div id="sessions-kanban" class="sessions-kanban" hidden></div>
|
|
325
|
-
<dialog id="drawer"></dialog>
|
|
326
|
-
<dialog id="term-modal" class="term-modal"></dialog>
|
|
327
|
-
<dialog id="history-modal" class="history-modal"></dialog>
|
|
328
|
-
</section>`;
|
|
329
|
-
}
|
|
330
|
-
async function fetchPickerBots() {
|
|
178
|
+
export async function fetchPickerBots() {
|
|
331
179
|
try {
|
|
332
180
|
const r = await fetch('/api/groups');
|
|
333
181
|
if (!r.ok)
|
|
@@ -336,2343 +184,13 @@ async function fetchPickerBots() {
|
|
|
336
184
|
const bots = Array.isArray(data?.bots) ? data.bots : [];
|
|
337
185
|
return bots
|
|
338
186
|
.filter((b) => b && typeof b.larkAppId === 'string')
|
|
339
|
-
.map((b) => ({
|
|
187
|
+
.map((b) => ({
|
|
188
|
+
larkAppId: b.larkAppId,
|
|
189
|
+
botName: typeof b.botName === 'string' && b.botName ? b.botName : b.larkAppId,
|
|
190
|
+
}));
|
|
340
191
|
}
|
|
341
192
|
catch {
|
|
342
193
|
return [];
|
|
343
194
|
}
|
|
344
195
|
}
|
|
345
|
-
function renderCreateSessionForm(bots) {
|
|
346
|
-
const botRows = bots.map(b => `
|
|
347
|
-
<label class="cs-bot"><input type="checkbox" name="bot" value="${escapeHtml(b.larkAppId)}"> <span>${escapeHtml(b.botName)}</span></label>`).join('');
|
|
348
|
-
return `
|
|
349
|
-
<article class="cs-card">
|
|
350
|
-
<header><h3>${t('sessions.create.title')}</h3></header>
|
|
351
|
-
<form id="cs-form">
|
|
352
|
-
<label class="form-row">
|
|
353
|
-
<span>${t('sessions.create.content')}</span>
|
|
354
|
-
<textarea name="content" rows="5" placeholder="${escapeHtml(t('sessions.create.contentPlaceholder'))}" required></textarea>
|
|
355
|
-
</label>
|
|
356
|
-
<fieldset class="cs-bots">
|
|
357
|
-
<legend>${t('sessions.create.bots')}</legend>
|
|
358
|
-
${botRows || `<p class="cs-empty">${t('sessions.create.noBots')}</p>`}
|
|
359
|
-
</fieldset>
|
|
360
|
-
<fieldset class="cs-mode">
|
|
361
|
-
<legend>${t('sessions.create.mode')}</legend>
|
|
362
|
-
<label><input type="radio" name="mode" value="lead" checked> ${t('sessions.create.modeLead')}</label>
|
|
363
|
-
<label><input type="radio" name="mode" value="all"> ${t('sessions.create.modeAll')}</label>
|
|
364
|
-
<small>${t('sessions.create.modeHelp')}</small>
|
|
365
|
-
</fieldset>
|
|
366
|
-
<div class="cs-lead-row form-row" hidden>
|
|
367
|
-
<span>${t('sessions.create.lead')}</span>
|
|
368
|
-
<select name="lead"></select>
|
|
369
|
-
<small>${t('sessions.create.leadHelp')}</small>
|
|
370
|
-
</div>
|
|
371
|
-
<fieldset class="cs-column">
|
|
372
|
-
<legend>${t('sessions.create.column')}</legend>
|
|
373
|
-
<label><input type="radio" name="column" value="in_progress" checked> ${t('sessions.create.columnInProgress')}</label>
|
|
374
|
-
<label><input type="radio" name="column" value="backlog"> ${t('sessions.create.columnBacklog')}</label>
|
|
375
|
-
<small>${t('sessions.create.columnHelp')}</small>
|
|
376
|
-
</fieldset>
|
|
377
|
-
<details class="cs-advanced">
|
|
378
|
-
<summary>${t('sessions.create.advanced')}</summary>
|
|
379
|
-
<label class="form-row">
|
|
380
|
-
<span>${t('sessions.create.groupName')}</span>
|
|
381
|
-
<input type="text" name="name" maxlength="60" placeholder="${escapeHtml(t('sessions.create.groupNamePlaceholder'))}">
|
|
382
|
-
</label>
|
|
383
|
-
<label class="form-row">
|
|
384
|
-
<span>${t('sessions.create.workingDir')}</span>
|
|
385
|
-
<input type="text" name="bindWorkingDir" placeholder="e.g. ~/projects/foo">
|
|
386
|
-
<small>${t('sessions.create.workingDirHelp')}</small>
|
|
387
|
-
</label>
|
|
388
|
-
</details>
|
|
389
|
-
<div class="actions cs-actions">
|
|
390
|
-
<button type="submit" class="primary">${t('sessions.create.submit')}</button>
|
|
391
|
-
<button type="button" id="cs-cancel">${t('sessions.create.cancel')}</button>
|
|
392
|
-
</div>
|
|
393
|
-
</form>
|
|
394
|
-
</article>`;
|
|
395
|
-
}
|
|
396
|
-
// 打开全局「创建会话」弹窗。按钮已提到顶栏、弹窗 #create-session-modal 挂在全局
|
|
397
|
-
// chrome(index.html),任意页面均可拉起。app.ts 的顶栏按钮以动态 import 调用本函数,
|
|
398
|
-
// 从而把 sessions 模块留在懒加载 chunk 里、不撑大主包。弹窗缺失时静默返回。
|
|
399
|
-
export async function openCreateSessionModal() {
|
|
400
|
-
const modal = document.getElementById('create-session-modal');
|
|
401
|
-
if (!modal)
|
|
402
|
-
return;
|
|
403
|
-
const bots = await fetchPickerBots();
|
|
404
|
-
if (bots.length === 0) {
|
|
405
|
-
alert(t('sessions.create.noBots'));
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
modal.innerHTML = renderCreateSessionForm(bots);
|
|
409
|
-
modal.showModal();
|
|
410
|
-
wireCreateSessionForm(modal, bots);
|
|
411
|
-
}
|
|
412
|
-
function wireCreateSessionForm(modal, bots) {
|
|
413
|
-
const form = modal.querySelector('#cs-form');
|
|
414
|
-
const leadRow = modal.querySelector('.cs-lead-row');
|
|
415
|
-
const leadSelect = modal.querySelector('select[name=lead]');
|
|
416
|
-
const nameOf = (id) => bots.find(b => b.larkAppId === id)?.botName ?? id;
|
|
417
|
-
const checkedBotIds = () => Array.from(form.querySelectorAll('input[name=bot]:checked')).map(i => i.value);
|
|
418
|
-
const currentMode = () => (form.querySelector('input[name=mode]:checked')?.value) ?? 'all';
|
|
419
|
-
// lead 下拉只列「已勾选」的 bot;勾选/模式变化时重建,尽量保留原选中项。
|
|
420
|
-
function repopulateLead() {
|
|
421
|
-
const ids = checkedBotIds();
|
|
422
|
-
const prev = leadSelect.value;
|
|
423
|
-
// 没勾选任何机器人时,空 <select> 会渲染成一个空白小框,看着像 bug——给一个
|
|
424
|
-
// 禁用的占位项并禁用整个选择器,提示先去上面勾选。
|
|
425
|
-
if (ids.length === 0) {
|
|
426
|
-
leadSelect.innerHTML = `<option value="" disabled selected>${escapeHtml(t('sessions.create.leadPickFirst'))}</option>`;
|
|
427
|
-
leadSelect.disabled = true;
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
leadSelect.disabled = false;
|
|
431
|
-
leadSelect.innerHTML = ids.map(id => `<option value="${escapeHtml(id)}">${escapeHtml(nameOf(id))}</option>`).join('');
|
|
432
|
-
if (ids.includes(prev))
|
|
433
|
-
leadSelect.value = prev;
|
|
434
|
-
}
|
|
435
|
-
function syncMode() {
|
|
436
|
-
if (currentMode() === 'lead') {
|
|
437
|
-
leadRow.hidden = false;
|
|
438
|
-
repopulateLead();
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
leadRow.hidden = true;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
form.querySelectorAll('input[name=mode]').forEach(r => r.addEventListener('change', syncMode));
|
|
445
|
-
form.querySelectorAll('input[name=bot]').forEach(c => c.addEventListener('change', () => {
|
|
446
|
-
if (currentMode() === 'lead')
|
|
447
|
-
repopulateLead();
|
|
448
|
-
}));
|
|
449
|
-
// 按默认模式(Lead 分配)初始化 Lead 行的显隐——一起开工时整行不展示。
|
|
450
|
-
syncMode();
|
|
451
|
-
modal.querySelector('#cs-cancel').onclick = () => modal.close();
|
|
452
|
-
form.onsubmit = async (ev) => {
|
|
453
|
-
ev.preventDefault();
|
|
454
|
-
const fd = new FormData(form);
|
|
455
|
-
const content = (fd.get('content') ?? '').trim();
|
|
456
|
-
const larkAppIds = checkedBotIds();
|
|
457
|
-
const mode = currentMode();
|
|
458
|
-
const column = (fd.get('column') ?? 'in_progress');
|
|
459
|
-
const name = (fd.get('name') ?? '').trim();
|
|
460
|
-
const bindWorkingDir = (fd.get('bindWorkingDir') ?? '').trim();
|
|
461
|
-
const leadLarkAppId = (fd.get('lead') ?? '');
|
|
462
|
-
if (!content) {
|
|
463
|
-
alert(t('sessions.create.errContent'));
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (larkAppIds.length === 0) {
|
|
467
|
-
alert(t('sessions.create.errNoBot'));
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
if (mode === 'lead' && (!leadLarkAppId || !larkAppIds.includes(leadLarkAppId))) {
|
|
471
|
-
alert(t('sessions.create.errLead'));
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
const submitBtn = form.querySelector('button[type=submit]');
|
|
475
|
-
if (submitBtn) {
|
|
476
|
-
submitBtn.disabled = true;
|
|
477
|
-
submitBtn.textContent = t('sessions.create.submitting');
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
const r = await fetch('/api/sessions/create', {
|
|
481
|
-
method: 'POST',
|
|
482
|
-
headers: { 'content-type': 'application/json' },
|
|
483
|
-
body: JSON.stringify({
|
|
484
|
-
content, larkAppIds, mode, column,
|
|
485
|
-
leadLarkAppId: mode === 'lead' ? leadLarkAppId : undefined,
|
|
486
|
-
name: name || undefined,
|
|
487
|
-
bindWorkingDir: bindWorkingDir || undefined,
|
|
488
|
-
}),
|
|
489
|
-
});
|
|
490
|
-
const body = await r.json().catch(() => null);
|
|
491
|
-
if (r.ok && body?.ok) {
|
|
492
|
-
renderCreateSessionSuccess(modal, body);
|
|
493
|
-
}
|
|
494
|
-
else if (r.status !== 401) {
|
|
495
|
-
alert(`${t('sessions.create.failed')}: ${body?.error ?? r.status}`);
|
|
496
|
-
if (submitBtn) {
|
|
497
|
-
submitBtn.disabled = false;
|
|
498
|
-
submitBtn.textContent = t('sessions.create.submit');
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
catch (e) {
|
|
503
|
-
alert(`${t('sessions.create.failed')}: ${e}`);
|
|
504
|
-
if (submitBtn) {
|
|
505
|
-
submitBtn.disabled = false;
|
|
506
|
-
submitBtn.textContent = t('sessions.create.submit');
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function renderCreateSessionSuccess(modal, body) {
|
|
512
|
-
const link = typeof body.shareLink === 'string' && body.shareLink ? body.shareLink : '';
|
|
513
|
-
const failedN = Array.isArray(body.failed) ? body.failed.length : 0;
|
|
514
|
-
const spawnedN = Array.isArray(body.spawned) ? body.spawned.length : 0;
|
|
515
|
-
const colNote = body.column === 'backlog' ? t('sessions.create.doneBacklog') : t('sessions.create.doneInProgress');
|
|
516
|
-
const failNote = failedN > 0 ? `<p class="cs-warn">${t('sessions.create.partialFail', { n: String(failedN) })}</p>` : '';
|
|
517
|
-
modal.innerHTML = `
|
|
518
|
-
<article class="cs-card">
|
|
519
|
-
<header><h3>${t('sessions.create.doneTitle')}</h3></header>
|
|
520
|
-
<p>${escapeHtml(colNote)}(${spawnedN})</p>
|
|
521
|
-
${failNote}
|
|
522
|
-
${link ? `<p><a href="${escapeHtml(link)}" target="_blank" rel="noopener">${t('sessions.create.openChat')}</a></p>` : ''}
|
|
523
|
-
<div class="actions"><button type="button" id="cs-done" class="primary">${t('sessions.create.close')}</button></div>
|
|
524
|
-
</article>`;
|
|
525
|
-
modal.querySelector('#cs-done').onclick = () => modal.close();
|
|
526
|
-
}
|
|
527
|
-
export function renderSessionsPage(root) {
|
|
528
|
-
root.innerHTML = pageHtml();
|
|
529
|
-
return wireSessionsPage(root);
|
|
530
|
-
}
|
|
531
|
-
export function wireSessionsPage(root) {
|
|
532
|
-
const tbody = root.querySelector('#sessions-table tbody');
|
|
533
|
-
const filtersForm = root.querySelector('#filters');
|
|
534
|
-
const chatSelect = root.querySelector('select[name="chat"]');
|
|
535
|
-
const showUnknownChatsInput = root.querySelector('input[name="showUnknownChats"]');
|
|
536
|
-
const drawer = root.querySelector('#drawer');
|
|
537
|
-
const selectAllBox = root.querySelector('#select-all');
|
|
538
|
-
const bulkBar = root.querySelector('#bulk-bar');
|
|
539
|
-
const bulkCountSpan = root.querySelector('#bulk-count');
|
|
540
|
-
const bulkMonitorRoomBtn = root.querySelector('#bulk-monitor-room');
|
|
541
|
-
const bulkLockBtn = root.querySelector('#bulk-lock');
|
|
542
|
-
const bulkUnlockBtn = root.querySelector('#bulk-unlock');
|
|
543
|
-
const bulkCloseBtn = root.querySelector('#bulk-close');
|
|
544
|
-
const bulkClearBtn = root.querySelector('#bulk-clear');
|
|
545
|
-
const idleCleanupBar = root.querySelector('#idle-cleanup-bar');
|
|
546
|
-
const idleCleanupThreshold = root.querySelector('#idle-cleanup-threshold');
|
|
547
|
-
const idleCleanupBtn = root.querySelector('#idle-cleanup-run');
|
|
548
|
-
const idleCleanupCount = root.querySelector('#idle-cleanup-count');
|
|
549
|
-
const idleCleanupStatus = root.querySelector('#idle-cleanup-status');
|
|
550
|
-
const table = root.querySelector('#sessions-table');
|
|
551
|
-
const board = root.querySelector('#sessions-board');
|
|
552
|
-
const kanban = root.querySelector('#sessions-kanban');
|
|
553
|
-
const termModal = root.querySelector('#term-modal');
|
|
554
|
-
const historyModal = root.querySelector('#history-modal');
|
|
555
|
-
const groupByBox = root.querySelector('#kanban-groupby');
|
|
556
|
-
const teamSelect = root.querySelector('#kanban-team');
|
|
557
|
-
const teamStats = root.querySelector('#kanban-team-stats');
|
|
558
|
-
const viewButtons = root.querySelectorAll('.sessions-view-toggle [data-view]');
|
|
559
|
-
const monitorRoomOpenBtn = root.querySelector('#monitor-room-open');
|
|
560
|
-
// 「创建会话」按钮 + 弹窗已提到全局顶栏,由 wireCreateSessionButton() 一次性接线(见 app.ts)。
|
|
561
|
-
const selected = new Set();
|
|
562
|
-
let sortKey = 'lastMessageAt';
|
|
563
|
-
let sortDir = 'desc';
|
|
564
|
-
let viewMode = readStoredSessionsViewMode(window.localStorage);
|
|
565
|
-
// 列顺序用户可调(拖列头 / ‹› 按钮),localStorage 持久化
|
|
566
|
-
let boardOrder = readStoredBoardOrder(window.localStorage);
|
|
567
|
-
let dragColId = null;
|
|
568
|
-
// 防闪烁:上次渲染的 HTML 快照(内容没变就跳过 innerHTML),以及
|
|
569
|
-
// 入场动画是否已播过(只在首次渲染播一轮)。
|
|
570
|
-
let lastBoardHtml = '';
|
|
571
|
-
let lastTableHtml = '';
|
|
572
|
-
let lastKanbanHtml = '';
|
|
573
|
-
let lastChatFilterHtml = '';
|
|
574
|
-
let boardAnimated = false;
|
|
575
|
-
// 看板交互态:拖拽中的卡片 id / 标题就地编辑中 —— 两者期间都跳过看板重绘,
|
|
576
|
-
// 否则 SSE 触发的 innerHTML 重建会把拖拽源/输入框拍没。
|
|
577
|
-
let kanbanDragId = null;
|
|
578
|
-
let kanbanEditing = false;
|
|
579
|
-
// 单击开终端 vs 双击改标题的仲裁:单击延迟 220ms 执行,双击先到就取消。
|
|
580
|
-
let kanbanOpenTimer = null;
|
|
581
|
-
// 上次渲染时每列的有序行(聚簇后的视觉平铺顺序)—— drop 落点据此找相邻卡片
|
|
582
|
-
// 算持久化位置。
|
|
583
|
-
let lastKanbanGroups = new Map();
|
|
584
|
-
// 看板分组维度:flow=工作流五列(可拖拽);team=选定团队的工作流看板(含
|
|
585
|
-
// 团队内所有 bot 的会话,可拖拽);bot=机器人视角列(只读总览)
|
|
586
|
-
let kanbanGroupBy = readStoredKanbanGroupBy(window.localStorage);
|
|
587
|
-
// 整簇拖拽:拖群组容器头部时记录 (chatId, 源列),drop 时整组搬运
|
|
588
|
-
let kanbanDragClusterChat = null;
|
|
589
|
-
let kanbanDragClusterCol = null;
|
|
590
|
-
// 团队清单(groupBy='team' 首次激活时懒加载:本地托管团队 + 远程 roster)。
|
|
591
|
-
// botNames 用来与 /introduce 记录的外部 bot 按名字匹配(introduce 只留
|
|
592
|
-
// openId+name,而 open_id 是 app-scoped 的,名字是两边唯一的公共标识)。
|
|
593
|
-
let kanbanTeams = [];
|
|
594
|
-
// 群 → { 在场自家 bot 集合, introduce 过的外部 bot 名字集合 }(/api/groups)。
|
|
595
|
-
let kanbanChatBots = null;
|
|
596
|
-
let kanbanTeamsLoaded = false;
|
|
597
|
-
let kanbanTeamsLoading = false;
|
|
598
|
-
let kanbanTeamKey = (() => {
|
|
599
|
-
try {
|
|
600
|
-
return window.localStorage.getItem(KANBAN_TEAM_STORAGE_KEY) ?? '';
|
|
601
|
-
}
|
|
602
|
-
catch {
|
|
603
|
-
return '';
|
|
604
|
-
}
|
|
605
|
-
})();
|
|
606
|
-
let idleCleanupBusy = false;
|
|
607
|
-
let idleCleanupHours = 24;
|
|
608
|
-
if (showUnknownChatsInput) {
|
|
609
|
-
showUnknownChatsInput.checked = readStoredSessionsShowUnknownChats(window.localStorage);
|
|
610
|
-
}
|
|
611
|
-
function selectedIdleCleanupHours() {
|
|
612
|
-
return idleCleanupHours;
|
|
613
|
-
}
|
|
614
|
-
function idleCleanupLabel(hours) {
|
|
615
|
-
return hours === 168 ? '7d' : `${hours}H`;
|
|
616
|
-
}
|
|
617
|
-
/** Chat IDs that belong to `team` in the kanban "team" view — the same
|
|
618
|
-
* whitelist renderKanban uses to narrow the board (team-bound group chats +
|
|
619
|
-
* groups where a same-team bot was /introduce'd). Extracted so idle cleanup
|
|
620
|
-
* can scope to exactly what the team board shows. */
|
|
621
|
-
function teamChatIdsFor(team) {
|
|
622
|
-
const teamChats = new Set();
|
|
623
|
-
if (!team)
|
|
624
|
-
return teamChats;
|
|
625
|
-
for (const chatId of team.groupChats)
|
|
626
|
-
teamChats.add(chatId);
|
|
627
|
-
if (kanbanChatBots) {
|
|
628
|
-
for (const [chatId, c] of kanbanChatBots) {
|
|
629
|
-
if (teamChats.has(chatId))
|
|
630
|
-
continue;
|
|
631
|
-
let hasTeamBot = false;
|
|
632
|
-
for (const id of team.botIds) {
|
|
633
|
-
if (c.botIds.has(id)) {
|
|
634
|
-
hasTeamBot = true;
|
|
635
|
-
break;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (!hasTeamBot)
|
|
639
|
-
continue;
|
|
640
|
-
for (const n of c.observedNames) {
|
|
641
|
-
if (team.botNames.has(n)) {
|
|
642
|
-
teamChats.add(chatId);
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return teamChats;
|
|
649
|
-
}
|
|
650
|
-
/** Local rows actually visible in the current view — the basis for idle
|
|
651
|
-
* cleanup so "所见即所关" holds everywhere. Table/board and kanban
|
|
652
|
-
* (by-status / by-bot) render the full `filtered()` set; only the team
|
|
653
|
-
* kanban narrows further to the selected team's chats AND overlays remote
|
|
654
|
-
* rows from other deployments (which this dashboard can't close). Mirror the
|
|
655
|
-
* team narrowing and never include remote rows. */
|
|
656
|
-
function currentCleanupVisibleRows() {
|
|
657
|
-
const rows = filtered();
|
|
658
|
-
if (viewMode === 'kanban' && kanbanGroupBy === 'team') {
|
|
659
|
-
const team = kanbanTeams.find(tm => tm.key === kanbanTeamKey) ?? kanbanTeams[0];
|
|
660
|
-
const teamChats = teamChatIdsFor(team);
|
|
661
|
-
return rows.filter(r => teamChats.has(String(r.chatId)));
|
|
662
|
-
}
|
|
663
|
-
return rows;
|
|
664
|
-
}
|
|
665
|
-
function currentIdleCleanupCandidates() {
|
|
666
|
-
// Scope to the rows actually visible in the current view (WYSIWYG): the
|
|
667
|
-
// count, the confirm dialog, and the sessionIds we POST all describe exactly
|
|
668
|
-
// what the operator sees — never other bots'/teams' sessions off-screen.
|
|
669
|
-
return selectIdleCleanupCandidates(currentCleanupVisibleRows(), selectedIdleCleanupHours());
|
|
670
|
-
}
|
|
671
|
-
function paintIdleCleanupThresholds() {
|
|
672
|
-
idleCleanupThreshold.querySelectorAll('button[data-hours]').forEach(btn => {
|
|
673
|
-
const active = parseIdleCleanupHours(btn.dataset.hours) === idleCleanupHours;
|
|
674
|
-
btn.classList.toggle('active', active);
|
|
675
|
-
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
async function loadKanbanTeams() {
|
|
679
|
-
if (kanbanTeamsLoading || kanbanTeamsLoaded)
|
|
680
|
-
return;
|
|
681
|
-
kanbanTeamsLoading = true;
|
|
682
|
-
try {
|
|
683
|
-
const [hosted, remote, groups] = await Promise.all([
|
|
684
|
-
fetch('/api/team/hosted').then(r => r.json()).catch(() => null),
|
|
685
|
-
fetch('/api/team/remote-roster').then(r => r.json()).catch(() => null),
|
|
686
|
-
fetch('/api/groups').then(r => r.json()).catch(() => null),
|
|
687
|
-
]);
|
|
688
|
-
if (Array.isArray(groups?.chats)) {
|
|
689
|
-
kanbanChatBots = new Map(groups.chats.map((c) => [
|
|
690
|
-
String(c.chatId),
|
|
691
|
-
{
|
|
692
|
-
botIds: new Set((c.memberBots ?? []).filter((mb) => mb.inChat).map((mb) => String(mb.larkAppId))),
|
|
693
|
-
observedNames: new Set((c.observedBotNames ?? []).map((n) => String(n))),
|
|
694
|
-
},
|
|
695
|
-
]));
|
|
696
|
-
}
|
|
697
|
-
const rosterBots = (bots) => ({
|
|
698
|
-
ids: new Set(bots.map((b) => String(b.larkAppId))),
|
|
699
|
-
names: new Set(bots.map((b) => String(b.name ?? '')).filter(Boolean)),
|
|
700
|
-
});
|
|
701
|
-
const teams = [];
|
|
702
|
-
for (const tm of hosted?.teams ?? []) {
|
|
703
|
-
const { ids, names } = rosterBots(tm.bots ?? []);
|
|
704
|
-
teams.push({
|
|
705
|
-
key: `local:${tm.teamId}`,
|
|
706
|
-
label: tm.isDefault ? t('team.myHostedTeam') : String(tm.name ?? tm.teamId),
|
|
707
|
-
botIds: ids,
|
|
708
|
-
botNames: names,
|
|
709
|
-
groupChats: new Set((tm.groupChatIds ?? []).map((c) => String(c))),
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
for (const m of remote?.memberships ?? []) {
|
|
713
|
-
const { ids, names } = rosterBots(m.roster?.bots ?? []);
|
|
714
|
-
teams.push({
|
|
715
|
-
key: `${m.hubUrl}::${m.teamId}`,
|
|
716
|
-
label: String(m.teamName ?? m.teamId ?? m.hubUrl),
|
|
717
|
-
botIds: ids,
|
|
718
|
-
botNames: names,
|
|
719
|
-
// 远程团队发起的协作群绑定记录在 hub 侧,spoke 暂取不到
|
|
720
|
-
groupChats: new Set(),
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
kanbanTeams = teams;
|
|
724
|
-
}
|
|
725
|
-
finally {
|
|
726
|
-
kanbanTeamsLoaded = true;
|
|
727
|
-
kanbanTeamsLoading = false;
|
|
728
|
-
}
|
|
729
|
-
if (kanbanTeams.length && !kanbanTeams.some(tm => tm.key === kanbanTeamKey)) {
|
|
730
|
-
kanbanTeamKey = kanbanTeams[0].key;
|
|
731
|
-
}
|
|
732
|
-
delete teamSelect.dataset.loading;
|
|
733
|
-
teamSelect.disabled = kanbanTeams.length === 0;
|
|
734
|
-
teamSelect.innerHTML = kanbanTeams.length
|
|
735
|
-
? kanbanTeams.map(tm => `<option value="${escapeHtml(tm.key)}"${tm.key === kanbanTeamKey ? ' selected' : ''}>${escapeHtml(tm.label)}</option>`).join('')
|
|
736
|
-
: `<option value="">${escapeHtml(t('sessions.kanban.noTeam'))}</option>`;
|
|
737
|
-
lastKanbanHtml = '';
|
|
738
|
-
rerender();
|
|
739
|
-
}
|
|
740
|
-
// ── hub 团队看板(共享编排 + 对方部署会话快照)────────────────────────────
|
|
741
|
-
// 编排存团队 host:托管团队读本地 /api/team/board/local/<id>,加入的远程团队
|
|
742
|
-
// 经 spoke 代理 /api/team/remote-board 到 hub。30s 软刷新。
|
|
743
|
-
let kanbanTeamBoardData = null;
|
|
744
|
-
let kanbanTeamBoardKey = '';
|
|
745
|
-
let kanbanTeamBoardFetchedAt = 0;
|
|
746
|
-
let kanbanTeamBoardLoading = false;
|
|
747
|
-
// 对方部署的行不在 store 里——拖拽落点查这里
|
|
748
|
-
let kanbanRemoteRows = new Map();
|
|
749
|
-
async function ensureTeamBoard(team) {
|
|
750
|
-
const fresh = kanbanTeamBoardKey === team.key && Date.now() - kanbanTeamBoardFetchedAt < 30_000;
|
|
751
|
-
if (kanbanTeamBoardLoading || fresh)
|
|
752
|
-
return;
|
|
753
|
-
kanbanTeamBoardLoading = true;
|
|
754
|
-
try {
|
|
755
|
-
const isLocal = team.key.startsWith('local:');
|
|
756
|
-
const u = isLocal
|
|
757
|
-
? `/api/team/board/local/${encodeURIComponent(team.key.slice('local:'.length))}`
|
|
758
|
-
: `/api/team/remote-board?key=${encodeURIComponent(team.key)}`;
|
|
759
|
-
const r = await fetch(u);
|
|
760
|
-
const body = await r.json().catch(() => ({}));
|
|
761
|
-
if (!r.ok || body?.ok === false)
|
|
762
|
-
return;
|
|
763
|
-
const myDeploymentId = typeof body.deploymentId === 'string' ? body.deploymentId : null;
|
|
764
|
-
const remoteRows = [];
|
|
765
|
-
kanbanRemoteRows = new Map();
|
|
766
|
-
for (const rep of Array.isArray(body.reports) ? body.reports : []) {
|
|
767
|
-
// 远程团队的响应里含自己部署的上报——本地行走实时 store,跳过
|
|
768
|
-
if (myDeploymentId && rep.deploymentId === myDeploymentId)
|
|
769
|
-
continue;
|
|
770
|
-
for (const s of Array.isArray(rep.sessions) ? rep.sessions : []) {
|
|
771
|
-
const row = { ...s, remoteDeployment: rep.deploymentName || rep.deploymentId };
|
|
772
|
-
remoteRows.push(row);
|
|
773
|
-
kanbanRemoteRows.set(String(s.sessionId), row);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
kanbanTeamBoardData = {
|
|
777
|
-
board: body.board && typeof body.board === 'object' ? body.board : {},
|
|
778
|
-
remoteRows,
|
|
779
|
-
};
|
|
780
|
-
kanbanTeamBoardKey = team.key;
|
|
781
|
-
kanbanTeamBoardFetchedAt = Date.now();
|
|
782
|
-
lastKanbanHtml = '';
|
|
783
|
-
rerender();
|
|
784
|
-
}
|
|
785
|
-
catch {
|
|
786
|
-
// 拉不到 hub 看板时退化为只看本地行
|
|
787
|
-
}
|
|
788
|
-
finally {
|
|
789
|
-
kanbanTeamBoardLoading = false;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
/** 团队看板拖拽落盘:写 host 的共享编排(不动会话的个人看板字段)。 */
|
|
793
|
-
async function persistTeamBoardMove(teamKey, sessionId, column, position, prevEntry) {
|
|
794
|
-
try {
|
|
795
|
-
const isLocal = teamKey.startsWith('local:');
|
|
796
|
-
const r = isLocal
|
|
797
|
-
? await fetch(`/api/team/board/local/${encodeURIComponent(teamKey.slice('local:'.length))}/move`, {
|
|
798
|
-
method: 'POST',
|
|
799
|
-
headers: { 'content-type': 'application/json' },
|
|
800
|
-
body: JSON.stringify({ sessionId, column, position }),
|
|
801
|
-
})
|
|
802
|
-
: await fetch('/api/team/remote-board-move', {
|
|
803
|
-
method: 'POST',
|
|
804
|
-
headers: { 'content-type': 'application/json' },
|
|
805
|
-
body: JSON.stringify({ key: teamKey, sessionId, column, position }),
|
|
806
|
-
});
|
|
807
|
-
const body = await r.json().catch(() => ({}));
|
|
808
|
-
if (!r.ok || body?.ok === false) {
|
|
809
|
-
if (kanbanTeamBoardData) {
|
|
810
|
-
if (prevEntry)
|
|
811
|
-
kanbanTeamBoardData.board[sessionId] = prevEntry;
|
|
812
|
-
else
|
|
813
|
-
delete kanbanTeamBoardData.board[sessionId];
|
|
814
|
-
}
|
|
815
|
-
lastKanbanHtml = '';
|
|
816
|
-
rerender();
|
|
817
|
-
if (r.status !== 401)
|
|
818
|
-
alert(`${t('sessions.kanban.moveFail')}: ${body?.error ?? r.status}`);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
catch (e) {
|
|
822
|
-
if (kanbanTeamBoardData) {
|
|
823
|
-
if (prevEntry)
|
|
824
|
-
kanbanTeamBoardData.board[sessionId] = prevEntry;
|
|
825
|
-
else
|
|
826
|
-
delete kanbanTeamBoardData.board[sessionId];
|
|
827
|
-
}
|
|
828
|
-
lastKanbanHtml = '';
|
|
829
|
-
rerender();
|
|
830
|
-
alert(`${t('sessions.kanban.moveFail')}: ${e}`);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
/** 团队模式拖拽的统一落点:乐观写本地缓存的共享编排 + POST host。 */
|
|
834
|
-
function applyTeamBoardMove(sessionId, column, position) {
|
|
835
|
-
const team = kanbanTeams.find(tm => tm.key === kanbanTeamKey) ?? kanbanTeams[0];
|
|
836
|
-
if (!team)
|
|
837
|
-
return;
|
|
838
|
-
if (!kanbanTeamBoardData || kanbanTeamBoardKey !== team.key) {
|
|
839
|
-
// 首次拉取尚未完成也允许拖:先建本地空编排缓存,写入照常进行
|
|
840
|
-
kanbanTeamBoardData = { board: {}, remoteRows: kanbanTeamBoardData?.remoteRows ?? [] };
|
|
841
|
-
kanbanTeamBoardKey = team.key;
|
|
842
|
-
}
|
|
843
|
-
const prev = kanbanTeamBoardData.board[sessionId];
|
|
844
|
-
kanbanTeamBoardData.board[sessionId] = { column, position };
|
|
845
|
-
void persistTeamBoardMove(team.key, sessionId, column, position, prev);
|
|
846
|
-
}
|
|
847
|
-
function orderedBoardColumns() {
|
|
848
|
-
return boardOrder
|
|
849
|
-
.map(id => BOARD_COLUMNS.find(c => c.id === id))
|
|
850
|
-
.filter((c) => !!c);
|
|
851
|
-
}
|
|
852
|
-
function moveColumn(id, delta) {
|
|
853
|
-
const from = boardOrder.indexOf(id);
|
|
854
|
-
const to = from + delta;
|
|
855
|
-
if (from < 0 || to < 0 || to >= boardOrder.length)
|
|
856
|
-
return;
|
|
857
|
-
const next = [...boardOrder];
|
|
858
|
-
next.splice(from, 1);
|
|
859
|
-
next.splice(to, 0, id);
|
|
860
|
-
boardOrder = next;
|
|
861
|
-
writeStoredBoardOrder(window.localStorage, boardOrder);
|
|
862
|
-
rerender();
|
|
863
|
-
}
|
|
864
|
-
function moveColumnTo(id, targetId) {
|
|
865
|
-
if (id === targetId)
|
|
866
|
-
return;
|
|
867
|
-
const from = boardOrder.indexOf(id);
|
|
868
|
-
const to = boardOrder.indexOf(targetId);
|
|
869
|
-
if (from < 0 || to < 0)
|
|
870
|
-
return;
|
|
871
|
-
const next = [...boardOrder];
|
|
872
|
-
next.splice(from, 1);
|
|
873
|
-
next.splice(to, 0, id);
|
|
874
|
-
boardOrder = next;
|
|
875
|
-
writeStoredBoardOrder(window.localStorage, boardOrder);
|
|
876
|
-
rerender();
|
|
877
|
-
}
|
|
878
|
-
function rowHtml(s) {
|
|
879
|
-
const closed = s.status === 'closed';
|
|
880
|
-
const checked = selected.has(s.sessionId) ? 'checked' : '';
|
|
881
|
-
const location = sessionLocationText(s);
|
|
882
|
-
return `<tr data-id="${escapeHtml(s.sessionId)}">
|
|
883
|
-
<td><input type="checkbox" class="row-select" ${checked} ${closed ? 'disabled' : ''}></td>
|
|
884
|
-
<td>${escapeHtml(botDisplayName(s))}</td>
|
|
885
|
-
<td><span class="badge cli-${cssToken(s.cliId)}">${escapeHtml(s.cliId ?? 'unknown')}</span></td>
|
|
886
|
-
<td>${statusBadgeHtml(s.status)}${lockChipHtml(s)}</td>
|
|
887
|
-
<td class="session-location-cell" title="${escapeHtml(sessionLocationTitle(s))}">${escapeHtml(location)}</td>
|
|
888
|
-
<td class="token-cell">${formatTokenCount(s.tokenUsage?.in)}</td>
|
|
889
|
-
<td class="token-cell">${formatTokenCount(s.tokenUsage?.out)}</td>
|
|
890
|
-
<td title="${escapeHtml(String(s.title ?? ''))}">${escapeHtml(stripMentionPrefix(s.title ?? '').slice(0, 48))}</td>
|
|
891
|
-
<td title="${escapeHtml(s.workingDir ?? '')}">${escapeHtml((s.workingDir ?? '').slice(-34))}</td>
|
|
892
|
-
<td>${relTime(s.spawnedAt)}</td>
|
|
893
|
-
<td>${relTime(s.lastMessageAt)}</td>
|
|
894
|
-
<td>${s.adopt ? '<span class="badge">adopt</span>' : ''}</td>
|
|
895
|
-
<td><button class="open" type="button">${t('sessions.details')}</button></td>
|
|
896
|
-
</tr>`;
|
|
897
|
-
}
|
|
898
|
-
// chat-scope(普通群/单聊平铺回复)没有话题可定位——「定位话题」换成直接
|
|
899
|
-
// 打开群聊的 applink。旧 daemon 上报的行没有 scope 字段,保持定位行为。
|
|
900
|
-
function chatScopeLink(s) {
|
|
901
|
-
if (s.scope !== 'chat' || !s.feishuChatLink)
|
|
902
|
-
return null;
|
|
903
|
-
const label = t('sessions.openChat');
|
|
904
|
-
return `<a class="card-act" href="${escapeHtml(s.feishuChatLink)}" target="_blank" rel="noopener" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}">${ICON.feishu}</a>`;
|
|
905
|
-
}
|
|
906
|
-
function boardSignalLabel(s) {
|
|
907
|
-
// Agent-raised reason is the most informative — show it verbatim so the
|
|
908
|
-
// human sees *why* the task is stuck, not a generic label.
|
|
909
|
-
if (s.agentAttention?.reason)
|
|
910
|
-
return s.agentAttention.reason;
|
|
911
|
-
if (s.agentAttention)
|
|
912
|
-
return t('sessions.board.signalAgent');
|
|
913
|
-
if (s.pendingRepo)
|
|
914
|
-
return t('sessions.board.signalRepo');
|
|
915
|
-
if (s.tuiPromptActive)
|
|
916
|
-
return t('sessions.board.signalPrompt');
|
|
917
|
-
if (s.status === 'limited')
|
|
918
|
-
return t('sessions.board.signalLimited');
|
|
919
|
-
return '';
|
|
920
|
-
}
|
|
921
|
-
function boardCardHtml(s) {
|
|
922
|
-
const isSelected = selected.has(s.sessionId);
|
|
923
|
-
// title 剥掉开头的 "@bot" mention,只留消息内容;副行的群聊名比 cliId 更
|
|
924
|
-
// 有辨识度(同一 bot 可能在多个群干活),查不到群名时回退 cliId。
|
|
925
|
-
const title = stripMentionPrefix(s.title) || s.sessionId;
|
|
926
|
-
const botName = botDisplayName(s);
|
|
927
|
-
const chatTitle = chatDisplayTitle(s);
|
|
928
|
-
const terminal = terminalHref(s);
|
|
929
|
-
const signal = boardSignalLabel(s);
|
|
930
|
-
const repo = repoBasename(s.workingDir);
|
|
931
|
-
return `<article class="session-card${isSelected ? ' selected' : ''}${s.locked ? ' locked' : ''}" data-id="${escapeHtml(s.sessionId)}" aria-pressed="${isSelected}">
|
|
932
|
-
<div class="session-card-top">
|
|
933
|
-
${botAvatarHtml({ name: botName, larkAppId: s.larkAppId, size: 'sm' })}
|
|
934
|
-
<div class="session-card-title">
|
|
935
|
-
<strong title="${escapeHtml(String(s.title ?? title))}">${escapeHtml(String(title).slice(0, 72))}</strong>
|
|
936
|
-
<span>${escapeHtml(botName)} · ${escapeHtml(chatTitle ?? s.cliId ?? 'unknown')}</span>
|
|
937
|
-
</div>
|
|
938
|
-
<span class="session-card-status-group">
|
|
939
|
-
${statusBadgeHtml(s.status)}
|
|
940
|
-
${lockChipHtml(s)}
|
|
941
|
-
</span>
|
|
942
|
-
</div>
|
|
943
|
-
${repo !== '-' || s.adopt ? `<div class="session-card-meta">
|
|
944
|
-
${repo !== '-' ? `<span title="${escapeHtml(s.workingDir ?? '')}">${escapeHtml(repo)}</span>` : ''}
|
|
945
|
-
${s.adopt ? '<span class="badge">adopt</span>' : ''}
|
|
946
|
-
</div>` : ''}
|
|
947
|
-
<div class="session-card-time">
|
|
948
|
-
<span>${s.agentAttention?.at
|
|
949
|
-
? `${escapeHtml(t('sessions.board.waiting'))} ${relTime(attentionWaitSince(s))}`
|
|
950
|
-
: `${escapeHtml(t('sessions.last'))}: ${relTime(s.lastMessageAt)}`}</span>
|
|
951
|
-
</div>
|
|
952
|
-
${signal ? `<div class="session-signal" title="${escapeHtml(signal)}">${escapeHtml(signal)}</div>` : ''}
|
|
953
|
-
<div class="session-card-actions">
|
|
954
|
-
${chatScopeLink(s) ?? cardActBtn('locate', ICON.pin, t('sessions.locate'))}
|
|
955
|
-
${cardActBtn('details', ICON.details, t('sessions.details'))}
|
|
956
|
-
${canRestartSession(s) ? cardActBtn('restart', ICON.restart, t('sessions.restart')) : ''}
|
|
957
|
-
${terminalControlsHtml(terminal)}
|
|
958
|
-
${cardActBtn('lock', s.locked ? ICON.unlock : ICON.lock, lockActionLabel(s), s.locked ? 'locked' : '')}
|
|
959
|
-
${cardActBtn('close', ICON.close, t('sessions.close'), 'danger')}
|
|
960
|
-
</div>
|
|
961
|
-
</article>`;
|
|
962
|
-
}
|
|
963
|
-
function compareBoardRows(a, b, column) {
|
|
964
|
-
const av = column === 'needs-you' ? attentionWaitSince(a) : Number(a.lastMessageAt ?? 0);
|
|
965
|
-
const bv = column === 'needs-you' ? attentionWaitSince(b) : Number(b.lastMessageAt ?? 0);
|
|
966
|
-
if (av !== bv)
|
|
967
|
-
return column === 'needs-you' ? av - bv : bv - av;
|
|
968
|
-
return String(a.title ?? a.sessionId).localeCompare(String(b.title ?? b.sessionId));
|
|
969
|
-
}
|
|
970
|
-
function renderBoard(rows) {
|
|
971
|
-
const groups = new Map(BOARD_COLUMNS.map(c => [c.id, []]));
|
|
972
|
-
for (const row of rows) {
|
|
973
|
-
const column = deriveSessionBoardColumn(row);
|
|
974
|
-
if (column)
|
|
975
|
-
groups.get(column).push(row);
|
|
976
|
-
}
|
|
977
|
-
const columns = orderedBoardColumns();
|
|
978
|
-
const html = columns.map((column, idx) => {
|
|
979
|
-
const columnRows = (groups.get(column.id) ?? []).sort((a, b) => compareBoardRows(a, b, column.id));
|
|
980
|
-
return `<section class="session-board-column session-board-${column.id}" data-col="${column.id}">
|
|
981
|
-
<header draggable="true" title="${escapeHtml(t('sessions.board.dragHint'))}">
|
|
982
|
-
<div>
|
|
983
|
-
<h2>${escapeHtml(t(column.labelKey))}</h2>
|
|
984
|
-
<p>${escapeHtml(t(column.hintKey))}</p>
|
|
985
|
-
</div>
|
|
986
|
-
<span class="session-board-head-right">
|
|
987
|
-
<span class="session-board-move">
|
|
988
|
-
<button type="button" data-move-col="${column.id}" data-dir="-1"
|
|
989
|
-
aria-label="${escapeHtml(t('sessions.board.moveLeft'))}" ${idx === 0 ? 'disabled' : ''}>‹</button>
|
|
990
|
-
<button type="button" data-move-col="${column.id}" data-dir="1"
|
|
991
|
-
aria-label="${escapeHtml(t('sessions.board.moveRight'))}" ${idx === columns.length - 1 ? 'disabled' : ''}>›</button>
|
|
992
|
-
</span>
|
|
993
|
-
<span class="session-board-count">${columnRows.length}</span>
|
|
994
|
-
</span>
|
|
995
|
-
</header>
|
|
996
|
-
<div class="session-board-list">
|
|
997
|
-
${columnRows.length ? columnRows.map(boardCardHtml).join('') : `<div class="session-board-empty">${t('sessions.board.emptyColumn')}</div>`}
|
|
998
|
-
</div>
|
|
999
|
-
</section>`;
|
|
1000
|
-
}).join('');
|
|
1001
|
-
// SSE 事件(心跳/无关字段 patch)远多于真正的可见变化——内容没变就不碰
|
|
1002
|
-
// DOM,避免 backdrop-blur 面板整板重排造成的闪烁。
|
|
1003
|
-
if (html === lastBoardHtml)
|
|
1004
|
-
return;
|
|
1005
|
-
lastBoardHtml = html;
|
|
1006
|
-
board.innerHTML = html;
|
|
1007
|
-
// 入场动画只在看板第一次画出来时播一轮(.board-enter),之后的状态更新
|
|
1008
|
-
// 重绘不再整板重播——那就是「每次状态更新页面闪一下」的来源。
|
|
1009
|
-
board.classList.toggle('board-enter', !boardAnimated);
|
|
1010
|
-
boardAnimated = true;
|
|
1011
|
-
}
|
|
1012
|
-
// ── 看板视图卡片 ─────────────────────────────────────────────────────────
|
|
1013
|
-
// 卡片整体即点击目标:单击开页面内终端弹窗;铅笔改标题;「详情」进抽屉;
|
|
1014
|
-
// 整卡可拖拽换列/排序。
|
|
1015
|
-
function kanbanCardHtml(s) {
|
|
1016
|
-
const title = stripMentionPrefix(s.title) || s.sessionId;
|
|
1017
|
-
const botName = botDisplayName(s);
|
|
1018
|
-
const chatTitle = chatDisplayTitle(s);
|
|
1019
|
-
const repo = repoBasename(s.workingDir);
|
|
1020
|
-
const signal = boardSignalLabel(s);
|
|
1021
|
-
const desc = [chatTitle, repo !== '-' ? repo : null].filter(Boolean).join(' · ');
|
|
1022
|
-
const status = String(s.status ?? 'unknown');
|
|
1023
|
-
// 对方部署的会话:数据是 host 快照,终端/历史/改名都在对方机器上做不了——
|
|
1024
|
-
// 只保留状态点与部署来源徽章,卡片仍可拖(团队共享编排)。
|
|
1025
|
-
const remote = typeof s.remoteDeployment === 'string' ? s.remoteDeployment : '';
|
|
1026
|
-
return `<article class="kanban-card${remote ? ' kanban-card-remote' : ''}${s.locked ? ' locked' : ''}" data-id="${escapeHtml(s.sessionId)}" tabindex="0" role="button" draggable="true">
|
|
1027
|
-
<div class="kanban-card-top">
|
|
1028
|
-
<span class="badge cli-${cssToken(s.cliId)}">${escapeHtml(s.cliId ?? 'unknown')}</span>
|
|
1029
|
-
${s.adopt ? '<span class="badge">adopt</span>' : ''}
|
|
1030
|
-
${lockChipHtml(s)}
|
|
1031
|
-
${remote ? `<span class="badge kanban-remote-badge" title="${escapeHtml(t('sessions.kanban.remoteHint', { name: remote }))}">${escapeHtml(remote)}</span>` : ''}
|
|
1032
|
-
<span class="kanban-card-top-right">
|
|
1033
|
-
<span class="kanban-card-dot" data-status="${escapeHtml(cssToken(status))}" title="${escapeHtml(sessionStatusText(status))}"></span>
|
|
1034
|
-
${remote ? '' : `<button type="button" class="card-act kanban-card-act" data-action="history" title="${escapeHtml(t('sessions.history.title'))}" aria-label="${escapeHtml(t('sessions.history.title'))}">${ICON.history}</button>
|
|
1035
|
-
${s.feishuChatLink ? `<a class="card-act kanban-card-act" href="${escapeHtml(s.feishuChatLink)}" target="_blank" rel="noopener" title="${escapeHtml(t('sessions.kanban.openFeishu'))}" aria-label="${escapeHtml(t('sessions.kanban.openFeishu'))}">${ICON.feishu}</a>` : ''}
|
|
1036
|
-
<button type="button" class="card-act kanban-card-act${s.locked ? ' locked' : ''}" data-action="lock" title="${escapeHtml(lockActionLabel(s))}" aria-label="${escapeHtml(lockActionLabel(s))}">${s.locked ? ICON.unlock : ICON.lock}</button>
|
|
1037
|
-
<button type="button" class="card-act kanban-card-act" data-action="details" title="${escapeHtml(t('sessions.details'))}" aria-label="${escapeHtml(t('sessions.details'))}">${ICON.details}</button>
|
|
1038
|
-
${canRestartSession(s) ? `<button type="button" class="card-act kanban-card-act" data-action="restart" title="${escapeHtml(t('sessions.restart'))}" aria-label="${escapeHtml(t('sessions.restart'))}">${ICON.restart}</button>` : ''}`}
|
|
1039
|
-
</span>
|
|
1040
|
-
</div>
|
|
1041
|
-
<p class="kanban-card-title" title="${escapeHtml(String(s.title ?? title))}">${escapeHtml(String(title).slice(0, 140))}</p>
|
|
1042
|
-
${desc ? `<p class="kanban-card-desc" title="${escapeHtml(desc)}">${escapeHtml(desc)}</p>` : ''}
|
|
1043
|
-
${signal ? `<div class="session-signal" title="${escapeHtml(signal)}">${escapeHtml(signal)}</div>` : ''}
|
|
1044
|
-
<div class="kanban-card-foot">
|
|
1045
|
-
<span class="kanban-card-owner">${botAvatarHtml({ name: botName, larkAppId: s.larkAppId, size: 'sm' })}<span>${escapeHtml(botName)}</span></span>
|
|
1046
|
-
<span class="kanban-card-updated">${escapeHtml(t('sessions.kanban.updated', { time: relTime(s.lastMessageAt) }))}</span>
|
|
1047
|
-
</div>
|
|
1048
|
-
</article>`;
|
|
1049
|
-
}
|
|
1050
|
-
/** 列内同群聚合:≥2 张同 chatId 的卡片折成群组容器(群头像 + 群名 + 计数),
|
|
1051
|
-
* 一眼看出它们关联同一个群/话题群;簇按首个成员出现位置参与列内排序。
|
|
1052
|
-
* 返回聚簇后 HTML 与视觉平铺顺序(drop 落点据此算相邻位置)。 */
|
|
1053
|
-
function clusteredListHtml(columnRows) {
|
|
1054
|
-
const order = [];
|
|
1055
|
-
const byChat = new Map();
|
|
1056
|
-
for (const r of columnRows) {
|
|
1057
|
-
const key = String(r.chatId ?? r.sessionId);
|
|
1058
|
-
let g = byChat.get(key);
|
|
1059
|
-
if (!g) {
|
|
1060
|
-
g = { chatId: key, rows: [] };
|
|
1061
|
-
byChat.set(key, g);
|
|
1062
|
-
order.push(g);
|
|
1063
|
-
}
|
|
1064
|
-
g.rows.push(r);
|
|
1065
|
-
}
|
|
1066
|
-
const flat = [];
|
|
1067
|
-
const html = order.map(g => {
|
|
1068
|
-
flat.push(...g.rows);
|
|
1069
|
-
if (g.rows.length < 2)
|
|
1070
|
-
return kanbanCardHtml(g.rows[0]);
|
|
1071
|
-
const title = chatDisplayTitle(g.rows[0]) ?? g.chatId;
|
|
1072
|
-
return `<div class="kanban-cluster" data-chat="${escapeHtml(g.chatId)}">
|
|
1073
|
-
<header draggable="true" title="${escapeHtml(title)} · ${escapeHtml(t('sessions.kanban.clusterDragHint'))}">
|
|
1074
|
-
${chatAvatarHtml({ chatId: g.chatId, name: title, size: 'sm' })}
|
|
1075
|
-
<span class="kanban-cluster-name">${escapeHtml(title)}</span>
|
|
1076
|
-
<span class="kanban-cluster-count">${g.rows.length}</span>
|
|
1077
|
-
</header>
|
|
1078
|
-
${g.rows.map(kanbanCardHtml).join('')}
|
|
1079
|
-
</div>`;
|
|
1080
|
-
}).join('');
|
|
1081
|
-
return { html, flat };
|
|
1082
|
-
}
|
|
1083
|
-
/** 团队模式:列 = 团队里的每个 bot(按名字排序),卡片 = 它名下的活跃会话,
|
|
1084
|
-
* 按最近活跃倒序、同群聚簇。协作总览视图——不支持拖拽(会话不能换 bot)。 */
|
|
1085
|
-
function kanbanByBotHtml(rows) {
|
|
1086
|
-
const bots = new Map();
|
|
1087
|
-
for (const r of rows) {
|
|
1088
|
-
if (r.status === 'closed')
|
|
1089
|
-
continue;
|
|
1090
|
-
const key = String(r.larkAppId || r.botName || 'unknown');
|
|
1091
|
-
let b = bots.get(key);
|
|
1092
|
-
if (!b) {
|
|
1093
|
-
b = { name: botDisplayName(r), larkAppId: r.larkAppId, rows: [] };
|
|
1094
|
-
bots.set(key, b);
|
|
1095
|
-
}
|
|
1096
|
-
b.rows.push(r);
|
|
1097
|
-
}
|
|
1098
|
-
const cols = [...bots.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1099
|
-
if (!cols.length)
|
|
1100
|
-
return `<div class="kanban-col-empty">${t('sessions.board.emptyColumn')}</div>`;
|
|
1101
|
-
return cols.map(col => {
|
|
1102
|
-
const colRows = col.rows.sort((a, b) => Number(b.lastMessageAt ?? 0) - Number(a.lastMessageAt ?? 0));
|
|
1103
|
-
const { html: listHtml } = clusteredListHtml(colRows);
|
|
1104
|
-
return `<section class="kanban-column kanban-bot-col" data-bot="${escapeHtml(col.larkAppId ?? col.name)}">
|
|
1105
|
-
<header>
|
|
1106
|
-
<span class="kanban-col-avatar">${botAvatarHtml({ name: col.name, larkAppId: col.larkAppId, size: 'sm' })}</span>
|
|
1107
|
-
<h2>${escapeHtml(col.name)}</h2>
|
|
1108
|
-
<span class="kanban-col-count">${colRows.length}</span>
|
|
1109
|
-
</header>
|
|
1110
|
-
<div class="kanban-col-list">${listHtml}</div>
|
|
1111
|
-
</section>`;
|
|
1112
|
-
}).join('');
|
|
1113
|
-
}
|
|
1114
|
-
/** 工作流五列看板(flow/team 共用):聚簇 + 拖拽落点数据。 */
|
|
1115
|
-
function kanbanFlowHtml(rows) {
|
|
1116
|
-
const groups = new Map(KANBAN_COLUMNS.map(c => [c.id, []]));
|
|
1117
|
-
for (const row of rows)
|
|
1118
|
-
groups.get(deriveKanbanColumn(row)).push(row);
|
|
1119
|
-
const html = KANBAN_COLUMNS.map(column => {
|
|
1120
|
-
let columnRows = (groups.get(column.id) ?? [])
|
|
1121
|
-
.sort((a, b) => effectiveKanbanPosition(a) - effectiveKanbanPosition(b));
|
|
1122
|
-
let hiddenCount = 0;
|
|
1123
|
-
if (column.id === 'done' && columnRows.length > KANBAN_DONE_CAP) {
|
|
1124
|
-
hiddenCount = columnRows.length - KANBAN_DONE_CAP;
|
|
1125
|
-
columnRows = columnRows.slice(0, KANBAN_DONE_CAP);
|
|
1126
|
-
}
|
|
1127
|
-
const { html: listHtml, flat } = clusteredListHtml(columnRows);
|
|
1128
|
-
groups.set(column.id, flat);
|
|
1129
|
-
return `<section class="kanban-column kanban-${column.id}" data-col="${column.id}">
|
|
1130
|
-
<header>
|
|
1131
|
-
<span class="kanban-col-icon">${kanbanStatusIcon(column.id)}</span>
|
|
1132
|
-
<h2>${escapeHtml(t(column.labelKey))}</h2>
|
|
1133
|
-
<span class="kanban-col-count">${columnRows.length + hiddenCount}</span>
|
|
1134
|
-
</header>
|
|
1135
|
-
<div class="kanban-col-list">
|
|
1136
|
-
${columnRows.length ? listHtml : `<div class="kanban-col-empty">${t('sessions.board.emptyColumn')}</div>`}
|
|
1137
|
-
${hiddenCount ? `<div class="kanban-col-more">${escapeHtml(t('sessions.kanban.moreHidden', { count: hiddenCount }))}</div>` : ''}
|
|
1138
|
-
</div>
|
|
1139
|
-
</section>`;
|
|
1140
|
-
}).join('');
|
|
1141
|
-
lastKanbanGroups = groups;
|
|
1142
|
-
return html;
|
|
1143
|
-
}
|
|
1144
|
-
function renderKanban(rows) {
|
|
1145
|
-
// 拖拽/编辑期间冻结 DOM —— innerHTML 重建会拍掉拖拽源和输入框。
|
|
1146
|
-
if (kanbanDragId || kanbanDragClusterChat || kanbanEditing)
|
|
1147
|
-
return;
|
|
1148
|
-
kanban.classList.toggle('kanban-mode-bot', kanbanGroupBy === 'bot');
|
|
1149
|
-
let html;
|
|
1150
|
-
if (kanbanGroupBy === 'bot') {
|
|
1151
|
-
html = kanbanByBotHtml(rows);
|
|
1152
|
-
lastKanbanGroups = new Map(); // 机器人视角无拖拽,不需要落点数据
|
|
1153
|
-
}
|
|
1154
|
-
else if (kanbanGroupBy === 'team') {
|
|
1155
|
-
if (!kanbanTeamsLoaded) {
|
|
1156
|
-
html = `<div class="kanban-loading">${t('sessions.kanban.teamLoading')}</div>`;
|
|
1157
|
-
lastKanbanGroups = new Map();
|
|
1158
|
-
// 加载期间下拉显示占位项并禁用——空胶囊很难看,也防止误操作
|
|
1159
|
-
if (!teamSelect.dataset.loading) {
|
|
1160
|
-
teamSelect.dataset.loading = '1';
|
|
1161
|
-
teamSelect.disabled = true;
|
|
1162
|
-
teamSelect.innerHTML = `<option>${escapeHtml(t('sessions.kanban.teamLoading'))}</option>`;
|
|
1163
|
-
}
|
|
1164
|
-
void loadKanbanTeams();
|
|
1165
|
-
}
|
|
1166
|
-
else {
|
|
1167
|
-
const team = kanbanTeams.find(tm => tm.key === kanbanTeamKey) ?? kanbanTeams[0];
|
|
1168
|
-
// 「团队群」白名单(既定规则):
|
|
1169
|
-
// A. dashboard 团队页发起的协作群(建群时落盘的 team↔chatId 绑定)
|
|
1170
|
-
// B. 群里 /introduce 过该团队成员机器人的群——介绍记录按名字与团队
|
|
1171
|
-
// roster 匹配;介绍过的若不是本团队成员,不算(防误筛)
|
|
1172
|
-
// 命中群里所有 bot 的会话都展示(本质 = 同团队 bot 所在群/话题的会话)。
|
|
1173
|
-
const teamChats = teamChatIdsFor(team);
|
|
1174
|
-
const teamRows = team ? rows.filter(r => teamChats.has(String(r.chatId))) : [];
|
|
1175
|
-
// ── hub 团队看板合并(既定架构:编排存团队 host)──────────────────────
|
|
1176
|
-
// 本地行(实时)+ 对方部署上报的裁剪行(host 快照);共享编排的列/排序
|
|
1177
|
-
// 覆盖个人看板字段——团队视图里大家看到同一份摆放。
|
|
1178
|
-
if (team)
|
|
1179
|
-
void ensureTeamBoard(team);
|
|
1180
|
-
const board = (kanbanTeamBoardKey === team?.key ? kanbanTeamBoardData?.board : null) ?? {};
|
|
1181
|
-
const remoteRows = (kanbanTeamBoardKey === team?.key ? kanbanTeamBoardData?.remoteRows : null) ?? [];
|
|
1182
|
-
const merged = [...teamRows, ...remoteRows].map(r => {
|
|
1183
|
-
const e = board[r.sessionId];
|
|
1184
|
-
return e ? { ...r, kanbanColumn: e.column, kanbanPosition: e.position } : r;
|
|
1185
|
-
});
|
|
1186
|
-
teamStats.textContent = t('sessions.kanban.teamScope', { chats: teamChats.size, sessions: merged.length });
|
|
1187
|
-
html = kanbanFlowHtml(merged);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
else {
|
|
1191
|
-
html = kanbanFlowHtml(rows);
|
|
1192
|
-
}
|
|
1193
|
-
if (html === lastKanbanHtml)
|
|
1194
|
-
return;
|
|
1195
|
-
lastKanbanHtml = html;
|
|
1196
|
-
// innerHTML 重建会把每列列表的滚动位置归零(改名失焦/SSE 更新时列表跳回
|
|
1197
|
-
// 顶部)——重建前按列记录 scrollTop,重建后恢复。
|
|
1198
|
-
const scrollTops = new Map();
|
|
1199
|
-
kanban.querySelectorAll('.kanban-col-list').forEach(el => {
|
|
1200
|
-
const col = el.closest('.kanban-column')?.dataset.col;
|
|
1201
|
-
if (col && el.scrollTop)
|
|
1202
|
-
scrollTops.set(col, el.scrollTop);
|
|
1203
|
-
});
|
|
1204
|
-
kanban.innerHTML = html;
|
|
1205
|
-
if (scrollTops.size) {
|
|
1206
|
-
kanban.querySelectorAll('.kanban-col-list').forEach(el => {
|
|
1207
|
-
const col = el.closest('.kanban-column')?.dataset.col;
|
|
1208
|
-
const top = col ? scrollTops.get(col) : undefined;
|
|
1209
|
-
if (top)
|
|
1210
|
-
el.scrollTop = top;
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
// ── 看板写操作:拖拽放置 / 重命名(乐观更新 + 失败回滚)────────────────────
|
|
1215
|
-
async function persistBoardMove(s, column, position, prev) {
|
|
1216
|
-
try {
|
|
1217
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/board`, {
|
|
1218
|
-
method: 'POST',
|
|
1219
|
-
headers: { 'content-type': 'application/json' },
|
|
1220
|
-
body: JSON.stringify({ column, position }),
|
|
1221
|
-
});
|
|
1222
|
-
const body = await r.json().catch(() => ({}));
|
|
1223
|
-
if (!r.ok || body?.ok === false) {
|
|
1224
|
-
s.kanbanColumn = prev.column;
|
|
1225
|
-
s.kanbanPosition = prev.position;
|
|
1226
|
-
lastKanbanHtml = '';
|
|
1227
|
-
rerender();
|
|
1228
|
-
// 401(只读访客)由全局 fetch patch 弹只读 toast,这里只负责回滚。
|
|
1229
|
-
if (r.status !== 401)
|
|
1230
|
-
alert(`${t('sessions.kanban.moveFail')}: ${body?.error ?? r.status}`);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
catch (e) {
|
|
1234
|
-
s.kanbanColumn = prev.column;
|
|
1235
|
-
s.kanbanPosition = prev.position;
|
|
1236
|
-
lastKanbanHtml = '';
|
|
1237
|
-
rerender();
|
|
1238
|
-
alert(`${t('sessions.kanban.moveFail')}: ${e}`);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
async function persistRename(s, title) {
|
|
1242
|
-
const prevTitle = s.title;
|
|
1243
|
-
s.title = title;
|
|
1244
|
-
lastKanbanHtml = '';
|
|
1245
|
-
rerender();
|
|
1246
|
-
try {
|
|
1247
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/rename`, {
|
|
1248
|
-
method: 'POST',
|
|
1249
|
-
headers: { 'content-type': 'application/json' },
|
|
1250
|
-
body: JSON.stringify({ title }),
|
|
1251
|
-
});
|
|
1252
|
-
const body = await r.json().catch(() => ({}));
|
|
1253
|
-
if (!r.ok || body?.ok === false) {
|
|
1254
|
-
s.title = prevTitle;
|
|
1255
|
-
lastKanbanHtml = '';
|
|
1256
|
-
rerender();
|
|
1257
|
-
if (r.status !== 401)
|
|
1258
|
-
alert(`${t('sessions.kanban.renameFail')}: ${body?.error ?? r.status}`);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
catch (e) {
|
|
1262
|
-
s.title = prevTitle;
|
|
1263
|
-
lastKanbanHtml = '';
|
|
1264
|
-
rerender();
|
|
1265
|
-
alert(`${t('sessions.kanban.renameFail')}: ${e}`);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
// ── 会话历史弹窗:实时拉取该会话所在飞书话题/群的消息,按聊天气泡渲染 ──────
|
|
1269
|
-
/** Lark create_time 是毫秒 epoch(数字或数字字符串)——直接 new Date(字符串)
|
|
1270
|
-
* 会得到 Invalid Date。统一转数字解析,解析不出就不显示。 */
|
|
1271
|
-
function historyTime(v) {
|
|
1272
|
-
if (v === undefined || v === null || v === '')
|
|
1273
|
-
return '';
|
|
1274
|
-
const n = Number(v);
|
|
1275
|
-
const d = Number.isFinite(n) && n > 0 ? new Date(n) : new Date(String(v));
|
|
1276
|
-
return Number.isNaN(d.getTime()) ? '' : d.toLocaleString();
|
|
1277
|
-
}
|
|
1278
|
-
function historyBubbleHtml(s, m, ownerOpenId) {
|
|
1279
|
-
const mine = m.senderType === 'user';
|
|
1280
|
-
// 后端经 contact API 补了 senderName/senderAvatar(可见范围内的真人);
|
|
1281
|
-
// 拿不到回退「创建者/用户」占位 + 首字圆。
|
|
1282
|
-
const name = mine
|
|
1283
|
-
? (m.senderName
|
|
1284
|
-
|| (ownerOpenId && m.senderId === ownerOpenId ? t('sessions.history.owner') : t('sessions.history.user')))
|
|
1285
|
-
: botDisplayName(s);
|
|
1286
|
-
const time = historyTime(m.createTime);
|
|
1287
|
-
const content = String(m.content ?? '').trim() || `[${m.msgType ?? 'message'}]`;
|
|
1288
|
-
const avatar = mine
|
|
1289
|
-
? (m.senderAvatar
|
|
1290
|
-
? `<img class="history-avatar-img" src="${escapeHtml(String(m.senderAvatar))}" alt="" decoding="async" referrerpolicy="no-referrer">`
|
|
1291
|
-
: `<span class="history-avatar-user" aria-hidden="true">${escapeHtml(String(name).slice(0, 1))}</span>`)
|
|
1292
|
-
: botAvatarHtml({ name: botDisplayName(s), larkAppId: s.larkAppId, size: 'sm' });
|
|
1293
|
-
return `<div class="history-msg${mine ? ' mine' : ''}">
|
|
1294
|
-
${avatar}
|
|
1295
|
-
<div class="history-msg-main">
|
|
1296
|
-
<div class="history-msg-meta"><span>${escapeHtml(name)}</span><time>${escapeHtml(time)}</time></div>
|
|
1297
|
-
<div class="history-bubble">${escapeHtml(content)}</div>
|
|
1298
|
-
</div>
|
|
1299
|
-
</div>`;
|
|
1300
|
-
}
|
|
1301
|
-
async function openHistoryModal(s) {
|
|
1302
|
-
const botName = botDisplayName(s);
|
|
1303
|
-
historyModal.innerHTML = `<div class="term-modal-head">
|
|
1304
|
-
<span class="term-modal-title">
|
|
1305
|
-
${botAvatarHtml({ name: botName, larkAppId: s.larkAppId, size: 'sm' })}
|
|
1306
|
-
<strong title="${escapeHtml(String(s.title ?? ''))}">${escapeHtml((stripMentionPrefix(s.title) || s.sessionId).slice(0, 60))}</strong>
|
|
1307
|
-
<span class="history-scope-tag">${escapeHtml(t('sessions.history.title'))}</span>
|
|
1308
|
-
</span>
|
|
1309
|
-
<span class="term-modal-actions">
|
|
1310
|
-
<button type="button" id="history-close" class="card-act" title="${escapeHtml(t('sessions.dismiss'))}" aria-label="${escapeHtml(t('sessions.dismiss'))}">${ICON.close}</button>
|
|
1311
|
-
</span>
|
|
1312
|
-
</div>
|
|
1313
|
-
<div class="history-body"><div class="term-modal-loading">${t('sessions.history.loading')}</div></div>`;
|
|
1314
|
-
historyModal.showModal();
|
|
1315
|
-
historyModal.querySelector('#history-close').onclick = () => historyModal.close();
|
|
1316
|
-
try {
|
|
1317
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/history?limit=80`);
|
|
1318
|
-
const body = await r.json().catch(() => ({}));
|
|
1319
|
-
if (!historyModal.open)
|
|
1320
|
-
return;
|
|
1321
|
-
const bodyEl = historyModal.querySelector('.history-body');
|
|
1322
|
-
if (!r.ok || body?.ok === false) {
|
|
1323
|
-
const errCode = String(body?.error ?? r.status);
|
|
1324
|
-
// not_found_yet = dashboard 进程没有该路由;not_found = daemon 没有 ——
|
|
1325
|
-
// 都是进程仍跑旧 build 的特征,明示重启而不是让人猜。
|
|
1326
|
-
const stale = errCode === 'not_found_yet' || errCode === 'not_found';
|
|
1327
|
-
bodyEl.innerHTML = `<div class="history-error">${escapeHtml(t('sessions.history.fail'))}: ${escapeHtml(errCode)}${stale ? `<br><span>${escapeHtml(t('sessions.history.staleHint'))}</span>` : ''}</div>`;
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
1331
|
-
if (!messages.length) {
|
|
1332
|
-
bodyEl.innerHTML = `<div class="history-error">${t('sessions.history.empty')}</div>`;
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
bodyEl.innerHTML = `<div class="history-list">${messages.map(m => historyBubbleHtml(s, m, body.ownerOpenId)).join('')}</div>`;
|
|
1336
|
-
bodyEl.scrollTop = bodyEl.scrollHeight; // 默认停在最新一条
|
|
1337
|
-
}
|
|
1338
|
-
catch (e) {
|
|
1339
|
-
if (!historyModal.open)
|
|
1340
|
-
return;
|
|
1341
|
-
const bodyEl = historyModal.querySelector('.history-body');
|
|
1342
|
-
if (bodyEl)
|
|
1343
|
-
bodyEl.innerHTML = `<div class="history-error">${escapeHtml(t('sessions.history.fail'))}: ${escapeHtml(String(e))}</div>`;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
/** 把卡片标题就地换成输入框:Enter/失焦保存,Esc 取消。 */
|
|
1347
|
-
function startKanbanRename(card, s) {
|
|
1348
|
-
const titleEl = card.querySelector('.kanban-card-title');
|
|
1349
|
-
if (!titleEl || card.querySelector('.kanban-rename-input'))
|
|
1350
|
-
return;
|
|
1351
|
-
kanbanEditing = true;
|
|
1352
|
-
const input = document.createElement('input');
|
|
1353
|
-
input.type = 'text';
|
|
1354
|
-
input.className = 'kanban-rename-input';
|
|
1355
|
-
input.maxLength = 200;
|
|
1356
|
-
input.value = stripMentionPrefix(s.title) || '';
|
|
1357
|
-
titleEl.replaceWith(input);
|
|
1358
|
-
input.focus();
|
|
1359
|
-
input.select();
|
|
1360
|
-
let settled = false;
|
|
1361
|
-
const finish = (commit) => {
|
|
1362
|
-
if (settled)
|
|
1363
|
-
return;
|
|
1364
|
-
settled = true;
|
|
1365
|
-
kanbanEditing = false;
|
|
1366
|
-
const next = input.value.trim();
|
|
1367
|
-
if (commit && next && next !== (stripMentionPrefix(s.title) || '')) {
|
|
1368
|
-
void persistRename(s, next);
|
|
1369
|
-
}
|
|
1370
|
-
else {
|
|
1371
|
-
lastKanbanHtml = '';
|
|
1372
|
-
rerender();
|
|
1373
|
-
}
|
|
1374
|
-
};
|
|
1375
|
-
input.addEventListener('keydown', ev => {
|
|
1376
|
-
ev.stopPropagation();
|
|
1377
|
-
if (ev.key === 'Enter') {
|
|
1378
|
-
ev.preventDefault();
|
|
1379
|
-
finish(true);
|
|
1380
|
-
}
|
|
1381
|
-
else if (ev.key === 'Escape') {
|
|
1382
|
-
ev.preventDefault();
|
|
1383
|
-
finish(false);
|
|
1384
|
-
}
|
|
1385
|
-
});
|
|
1386
|
-
input.addEventListener('blur', () => finish(true));
|
|
1387
|
-
input.addEventListener('click', ev => ev.stopPropagation());
|
|
1388
|
-
}
|
|
1389
|
-
/** 指针 Y 落点之下的第一张卡片(不含拖拽源)—— 新卡插它前面;null = 追加列尾。 */
|
|
1390
|
-
function kanbanInsertBeforeCard(column, clientY) {
|
|
1391
|
-
for (const card of column.querySelectorAll('.kanban-card:not(.dragging)')) {
|
|
1392
|
-
// 整簇拖拽时簇内成员不能当落点参照
|
|
1393
|
-
if (card.closest('.kanban-cluster.dragging'))
|
|
1394
|
-
continue;
|
|
1395
|
-
const rect = card.getBoundingClientRect();
|
|
1396
|
-
if (clientY < rect.top + rect.height / 2)
|
|
1397
|
-
return card;
|
|
1398
|
-
}
|
|
1399
|
-
return null;
|
|
1400
|
-
}
|
|
1401
|
-
function clearKanbanDragMarks() {
|
|
1402
|
-
kanban.querySelectorAll('.drag-over, .dragging, .drop-before')
|
|
1403
|
-
.forEach(el => el.classList.remove('drag-over', 'dragging', 'drop-before'));
|
|
1404
|
-
}
|
|
1405
|
-
// 终端弹窗标题就地改名:标题文本换输入框,Enter/失焦保存、Esc 取消;
|
|
1406
|
-
// 复用 persistRename(乐观更新 + 失败回滚 + 全视图同步)。
|
|
1407
|
-
function startTermTitleEdit(s) {
|
|
1408
|
-
const nameEl = termModal.querySelector('.term-modal-name');
|
|
1409
|
-
if (!nameEl || termModal.querySelector('.term-modal-name-input'))
|
|
1410
|
-
return;
|
|
1411
|
-
const input = document.createElement('input');
|
|
1412
|
-
input.type = 'text';
|
|
1413
|
-
input.className = 'term-modal-name-input';
|
|
1414
|
-
input.maxLength = 200;
|
|
1415
|
-
input.value = stripMentionPrefix(s.title) || '';
|
|
1416
|
-
nameEl.replaceWith(input);
|
|
1417
|
-
// 宽度贴合内容:按文本实测像素宽设宽,clamp 到 [80, 60vw],超长则到上限
|
|
1418
|
-
// 后内部横向滚动(与原标题省略号的「有上限」体感一致)。
|
|
1419
|
-
const fitInput = () => {
|
|
1420
|
-
const cs = getComputedStyle(input);
|
|
1421
|
-
const span = document.createElement('span');
|
|
1422
|
-
span.style.cssText = 'position:absolute;visibility:hidden;white-space:pre';
|
|
1423
|
-
span.style.fontSize = cs.fontSize;
|
|
1424
|
-
span.style.fontFamily = cs.fontFamily;
|
|
1425
|
-
span.style.fontWeight = cs.fontWeight;
|
|
1426
|
-
span.style.letterSpacing = cs.letterSpacing;
|
|
1427
|
-
span.textContent = input.value || ' ';
|
|
1428
|
-
document.body.appendChild(span);
|
|
1429
|
-
const w = span.offsetWidth;
|
|
1430
|
-
span.remove();
|
|
1431
|
-
const max = Math.round(window.innerWidth * 0.6);
|
|
1432
|
-
input.style.width = `${Math.min(Math.max(w + 22, 80), max)}px`;
|
|
1433
|
-
};
|
|
1434
|
-
fitInput();
|
|
1435
|
-
input.addEventListener('input', fitInput);
|
|
1436
|
-
input.focus();
|
|
1437
|
-
input.select();
|
|
1438
|
-
let settled = false;
|
|
1439
|
-
const finish = (commit) => {
|
|
1440
|
-
if (settled)
|
|
1441
|
-
return;
|
|
1442
|
-
settled = true;
|
|
1443
|
-
const next = input.value.trim();
|
|
1444
|
-
const cur = stripMentionPrefix(s.title) || '';
|
|
1445
|
-
if (commit && next && next !== cur) {
|
|
1446
|
-
s.title = next; // 弹窗内即时反映;persistRename 再发请求 + 背后 rerender
|
|
1447
|
-
const strong = document.createElement('strong');
|
|
1448
|
-
strong.className = 'term-modal-name';
|
|
1449
|
-
strong.title = next;
|
|
1450
|
-
strong.textContent = next.slice(0, 60);
|
|
1451
|
-
input.replaceWith(strong);
|
|
1452
|
-
void persistRename(s, next);
|
|
1453
|
-
}
|
|
1454
|
-
else {
|
|
1455
|
-
const strong = document.createElement('strong');
|
|
1456
|
-
strong.className = 'term-modal-name';
|
|
1457
|
-
strong.title = String(s.title ?? cur);
|
|
1458
|
-
strong.textContent = cur.slice(0, 60);
|
|
1459
|
-
input.replaceWith(strong);
|
|
1460
|
-
}
|
|
1461
|
-
};
|
|
1462
|
-
input.addEventListener('keydown', ev => {
|
|
1463
|
-
ev.stopPropagation();
|
|
1464
|
-
if (ev.key === 'Enter') {
|
|
1465
|
-
ev.preventDefault();
|
|
1466
|
-
finish(true);
|
|
1467
|
-
}
|
|
1468
|
-
else if (ev.key === 'Escape') {
|
|
1469
|
-
ev.preventDefault();
|
|
1470
|
-
finish(false);
|
|
1471
|
-
}
|
|
1472
|
-
});
|
|
1473
|
-
input.addEventListener('blur', () => finish(true));
|
|
1474
|
-
}
|
|
1475
|
-
// 页面内终端弹窗:默认嵌只读终端;已认证用户先 mint 可写链接(弹窗里能直接
|
|
1476
|
-
// 打字),拿不到再回退只读。没有 web 终端(挂起/已关闭)时退回详情抽屉。
|
|
1477
|
-
async function openTerminalModal(s) {
|
|
1478
|
-
const readonlyUrl = terminalHref(s);
|
|
1479
|
-
if (!readonlyUrl) {
|
|
1480
|
-
openDrawer(s);
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
const title = stripMentionPrefix(s.title) || s.sessionId;
|
|
1484
|
-
const feishu = s.feishuChatLink
|
|
1485
|
-
? `<a class="card-act" href="${escapeHtml(s.feishuChatLink)}" target="_blank" rel="noopener" title="${escapeHtml(t('sessions.kanban.openFeishu'))}" aria-label="${escapeHtml(t('sessions.kanban.openFeishu'))}">${ICON.feishu}</a>`
|
|
1486
|
-
: '';
|
|
1487
|
-
termModal.innerHTML = `<div class="term-modal-head">
|
|
1488
|
-
<span class="term-modal-title">
|
|
1489
|
-
${botAvatarHtml({ name: botDisplayName(s), larkAppId: s.larkAppId, size: 'sm' })}
|
|
1490
|
-
<strong class="term-modal-name" title="${escapeHtml(String(s.title ?? title))}">${escapeHtml(String(title).slice(0, 60))}</strong>
|
|
1491
|
-
<button type="button" id="term-modal-edit" class="card-act" title="${escapeHtml(t('sessions.kanban.rename'))}" aria-label="${escapeHtml(t('sessions.kanban.rename'))}">${ICON.edit}</button>
|
|
1492
|
-
${statusBadgeHtml(s.status)}
|
|
1493
|
-
</span>
|
|
1494
|
-
<span class="term-modal-actions">
|
|
1495
|
-
${feishu}
|
|
1496
|
-
<a id="term-modal-tab" class="card-act" href="${escapeHtml(readonlyUrl)}" target="_blank" rel="noopener" title="${escapeHtml(t('sessions.kanban.openTab'))}" aria-label="${escapeHtml(t('sessions.kanban.openTab'))}">${ICON.terminal}</a>
|
|
1497
|
-
<button type="button" id="term-modal-close" class="card-act" title="${escapeHtml(t('sessions.dismiss'))}" aria-label="${escapeHtml(t('sessions.dismiss'))}">${ICON.close}</button>
|
|
1498
|
-
</span>
|
|
1499
|
-
</div>
|
|
1500
|
-
<div class="term-modal-body"><div class="term-modal-loading">${t('sessions.kanban.terminalLoading')}</div></div>`;
|
|
1501
|
-
termModal.showModal();
|
|
1502
|
-
termModal.querySelector('#term-modal-close').onclick = () => termModal.close();
|
|
1503
|
-
termModal.querySelector('#term-modal-edit').onclick = () => startTermTitleEdit(s);
|
|
1504
|
-
let url = readonlyUrl;
|
|
1505
|
-
if (ui.authed) {
|
|
1506
|
-
try {
|
|
1507
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/write-link`);
|
|
1508
|
-
const body = await r.json().catch(() => ({}));
|
|
1509
|
-
if (r.ok && body?.ok !== false && body?.url)
|
|
1510
|
-
url = body.url;
|
|
1511
|
-
}
|
|
1512
|
-
catch {
|
|
1513
|
-
// 可写链接拿不到就用只读链接,弹窗仍可观看
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
if (!termModal.open)
|
|
1517
|
-
return; // 加载期间用户已关掉弹窗
|
|
1518
|
-
const bodyEl = termModal.querySelector('.term-modal-body');
|
|
1519
|
-
bodyEl.innerHTML = `<iframe class="term-modal-frame" src="${escapeHtml(url)}" allow="clipboard-read; clipboard-write"></iframe>`;
|
|
1520
|
-
const tab = termModal.querySelector('#term-modal-tab');
|
|
1521
|
-
if (tab)
|
|
1522
|
-
tab.href = url;
|
|
1523
|
-
}
|
|
1524
|
-
function filtered() {
|
|
1525
|
-
const f = new FormData(filtersForm);
|
|
1526
|
-
const q = (f.get('q') ?? '').toLowerCase();
|
|
1527
|
-
const cli = f.getAll('cli');
|
|
1528
|
-
const cliFilterActive = cli.length > 0 && cli.length < CLI_FILTER_OPTIONS.length;
|
|
1529
|
-
const status = f.get('status');
|
|
1530
|
-
const adopt = f.get('adopt');
|
|
1531
|
-
const chat = f.get('chat');
|
|
1532
|
-
const active = !!f.get('active');
|
|
1533
|
-
const showUnknownChats = !!f.get('showUnknownChats');
|
|
1534
|
-
// 看板视图的「已完成」列收纳已关闭会话——「仅活跃」开关不再把它们整体
|
|
1535
|
-
// 滤掉,否则该列永远是空的。
|
|
1536
|
-
const keepClosed = viewMode === 'kanban';
|
|
1537
|
-
const rows = [...store.sessions.values()]
|
|
1538
|
-
.filter(s => !cliFilterActive || cli.includes(s.cliId ?? 'unknown'))
|
|
1539
|
-
.filter(s => showUnknownChats || !isUnknownChatSession(s))
|
|
1540
|
-
.filter(s => !status || s.status === status)
|
|
1541
|
-
.filter(s => !adopt || (adopt === 'yes') === !!s.adopt)
|
|
1542
|
-
.filter(s => !chat || String(s.chatId ?? '') === chat)
|
|
1543
|
-
.filter(s => !active || keepClosed || s.status !== 'closed')
|
|
1544
|
-
.filter(s => !q || sessionSearchText(s).includes(q));
|
|
1545
|
-
rows.sort(compareRows);
|
|
1546
|
-
return rows;
|
|
1547
|
-
}
|
|
1548
|
-
function sortValue(s, key) {
|
|
1549
|
-
if (key === 'spawnedAt' || key === 'lastMessageAt')
|
|
1550
|
-
return Number(s[key] ?? 0);
|
|
1551
|
-
if (key === 'tokenIn')
|
|
1552
|
-
return tokenCount(s.tokenUsage?.in) ?? -1;
|
|
1553
|
-
if (key === 'tokenOut')
|
|
1554
|
-
return tokenCount(s.tokenUsage?.out) ?? -1;
|
|
1555
|
-
if (key === 'adopt')
|
|
1556
|
-
return !!s.adopt;
|
|
1557
|
-
if (key === 'chat')
|
|
1558
|
-
return sessionLocationText(s).toLowerCase();
|
|
1559
|
-
return String(s[key] ?? '').toLowerCase();
|
|
1560
|
-
}
|
|
1561
|
-
function compareRows(a, b) {
|
|
1562
|
-
const av = sortValue(a, sortKey);
|
|
1563
|
-
const bv = sortValue(b, sortKey);
|
|
1564
|
-
let cmp = 0;
|
|
1565
|
-
if (typeof av === 'number' && typeof bv === 'number')
|
|
1566
|
-
cmp = av - bv;
|
|
1567
|
-
else if (typeof av === 'boolean' && typeof bv === 'boolean')
|
|
1568
|
-
cmp = Number(av) - Number(bv);
|
|
1569
|
-
else
|
|
1570
|
-
cmp = String(av).localeCompare(String(bv));
|
|
1571
|
-
if (cmp === 0)
|
|
1572
|
-
cmp = Number(a.lastMessageAt ?? 0) - Number(b.lastMessageAt ?? 0);
|
|
1573
|
-
return sortDir === 'asc' ? cmp : -cmp;
|
|
1574
|
-
}
|
|
1575
|
-
function paintSortHeaders() {
|
|
1576
|
-
table.querySelectorAll('th[data-sort]').forEach(header => {
|
|
1577
|
-
const active = header.dataset.sort === sortKey;
|
|
1578
|
-
header.classList.toggle('sorted', active);
|
|
1579
|
-
header.setAttribute('aria-sort', active ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none');
|
|
1580
|
-
const label = header.dataset.label ?? header.textContent?.trim() ?? '';
|
|
1581
|
-
header.textContent = active ? `${label} ${sortDir === 'asc' ? '▲' : '▼'}` : label;
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
function syncBulkUi(rows) {
|
|
1585
|
-
bulkBar.hidden = selected.size === 0;
|
|
1586
|
-
bulkCountSpan.textContent = t('sessions.selectedCount', { count: selected.size });
|
|
1587
|
-
const selectedRows = [...selected]
|
|
1588
|
-
.map(id => store.sessions.get(id))
|
|
1589
|
-
.filter((r) => !!r && r.status !== 'closed');
|
|
1590
|
-
bulkMonitorRoomBtn.disabled = selectedRows.length === 0;
|
|
1591
|
-
bulkLockBtn.disabled = !selectedRows.some(r => !r.locked);
|
|
1592
|
-
bulkUnlockBtn.disabled = !selectedRows.some(r => !!r.locked);
|
|
1593
|
-
const selectable = rows.filter(r => r.status !== 'closed');
|
|
1594
|
-
if (selectable.length === 0) {
|
|
1595
|
-
selectAllBox.checked = false;
|
|
1596
|
-
selectAllBox.indeterminate = false;
|
|
1597
|
-
selectAllBox.disabled = true;
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
selectAllBox.disabled = false;
|
|
1601
|
-
const selectedInView = selectable.filter(r => selected.has(r.sessionId)).length;
|
|
1602
|
-
selectAllBox.checked = selectedInView === selectable.length;
|
|
1603
|
-
selectAllBox.indeterminate = selectedInView > 0 && selectedInView < selectable.length;
|
|
1604
|
-
}
|
|
1605
|
-
function syncIdleCleanupUi() {
|
|
1606
|
-
const count = currentIdleCleanupCandidates().length;
|
|
1607
|
-
idleCleanupCount.textContent = t('sessions.idleCleanupCount', { count });
|
|
1608
|
-
idleCleanupBtn.disabled = idleCleanupBusy || count === 0;
|
|
1609
|
-
// Keep the bar (and its threshold switcher) visible so the operator can
|
|
1610
|
-
// probe other thresholds, but drop the danger-red dot to neutral when
|
|
1611
|
-
// there's nothing to clean — a red alarm at 0 candidates is misleading.
|
|
1612
|
-
idleCleanupBar.classList.toggle('is-empty', count === 0);
|
|
1613
|
-
paintIdleCleanupThresholds();
|
|
1614
|
-
}
|
|
1615
|
-
function paintViewToggle() {
|
|
1616
|
-
viewButtons.forEach(btn => {
|
|
1617
|
-
const active = btn.dataset.view === viewMode;
|
|
1618
|
-
btn.classList.toggle('active', active);
|
|
1619
|
-
btn.setAttribute('aria-pressed', String(active));
|
|
1620
|
-
});
|
|
1621
|
-
groupByBox.hidden = viewMode !== 'kanban';
|
|
1622
|
-
teamSelect.hidden = !(viewMode === 'kanban' && kanbanGroupBy === 'team');
|
|
1623
|
-
teamStats.hidden = teamSelect.hidden || !kanbanTeamsLoaded;
|
|
1624
|
-
groupByBox.querySelectorAll('[data-groupby]').forEach(btn => {
|
|
1625
|
-
const active = btn.dataset.groupby === kanbanGroupBy;
|
|
1626
|
-
btn.classList.toggle('active', active);
|
|
1627
|
-
btn.setAttribute('aria-pressed', String(active));
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
// CLI 下拉 chip 上的已选计数:全选时显示「全部」,否则显示 N/总数
|
|
1631
|
-
function paintCliFilterCount() {
|
|
1632
|
-
const countEl = filtersForm.querySelector('#cli-filter-count');
|
|
1633
|
-
if (!countEl)
|
|
1634
|
-
return;
|
|
1635
|
-
const boxes = [...filtersForm.querySelectorAll('input[name="cli"]')];
|
|
1636
|
-
const checked = boxes.filter(b => b.checked).length;
|
|
1637
|
-
countEl.textContent = checked === boxes.length ? t('common.all') : `${checked}/${boxes.length}`;
|
|
1638
|
-
countEl.classList.toggle('cli-filter-active', checked !== boxes.length);
|
|
1639
|
-
}
|
|
1640
|
-
function syncChatFilterOptions() {
|
|
1641
|
-
const prev = chatSelect.value;
|
|
1642
|
-
const showUnknownChats = !!filtersForm.querySelector('input[name="showUnknownChats"]')?.checked;
|
|
1643
|
-
const options = new Map();
|
|
1644
|
-
for (const row of store.sessions.values()) {
|
|
1645
|
-
const chatId = String(row.chatId ?? '').trim();
|
|
1646
|
-
if (!chatId)
|
|
1647
|
-
continue;
|
|
1648
|
-
if (!showUnknownChats && isUnknownChatSession(row))
|
|
1649
|
-
continue;
|
|
1650
|
-
const label = sessionLocationText(row);
|
|
1651
|
-
const existing = options.get(chatId);
|
|
1652
|
-
if (!existing || label < existing)
|
|
1653
|
-
options.set(chatId, label);
|
|
1654
|
-
}
|
|
1655
|
-
const rows = [...options.entries()].sort((a, b) => a[1].localeCompare(b[1]));
|
|
1656
|
-
const html = `<option value="">${escapeHtml(t('sessions.chatAny'))}</option>${rows.map(([chatId, label]) => `<option value="${escapeHtml(chatId)}">${escapeHtml(label)}</option>`).join('')}`;
|
|
1657
|
-
if (html !== lastChatFilterHtml) {
|
|
1658
|
-
chatSelect.innerHTML = html;
|
|
1659
|
-
lastChatFilterHtml = html;
|
|
1660
|
-
}
|
|
1661
|
-
chatSelect.value = prev && options.has(prev) ? prev : '';
|
|
1662
|
-
}
|
|
1663
|
-
function rerender() {
|
|
1664
|
-
syncChatFilterOptions();
|
|
1665
|
-
const rows = filtered();
|
|
1666
|
-
for (const sid of [...selected]) {
|
|
1667
|
-
const s = store.sessions.get(sid);
|
|
1668
|
-
if (!s || s.status === 'closed')
|
|
1669
|
-
selected.delete(sid);
|
|
1670
|
-
}
|
|
1671
|
-
const boardRows = rows.filter(r => r.status !== 'closed');
|
|
1672
|
-
const visibleRows = viewMode === 'table' ? rows : boardRows;
|
|
1673
|
-
table.hidden = viewMode !== 'table';
|
|
1674
|
-
board.hidden = viewMode !== 'board';
|
|
1675
|
-
kanban.hidden = viewMode !== 'kanban';
|
|
1676
|
-
if (viewMode === 'table') {
|
|
1677
|
-
const tableHtml = rows.length
|
|
1678
|
-
? rows.map(rowHtml).join('')
|
|
1679
|
-
: `<tr><td colspan="13" class="empty">${t('sessions.empty')}</td></tr>`;
|
|
1680
|
-
if (tableHtml !== lastTableHtml) {
|
|
1681
|
-
lastTableHtml = tableHtml;
|
|
1682
|
-
tbody.innerHTML = tableHtml;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
else if (viewMode === 'kanban') {
|
|
1686
|
-
renderKanban(rows);
|
|
1687
|
-
}
|
|
1688
|
-
else {
|
|
1689
|
-
renderBoard(boardRows);
|
|
1690
|
-
}
|
|
1691
|
-
paintViewToggle();
|
|
1692
|
-
paintSortHeaders();
|
|
1693
|
-
paintCliFilterCount();
|
|
1694
|
-
syncBulkUi(visibleRows);
|
|
1695
|
-
syncIdleCleanupUi();
|
|
1696
|
-
}
|
|
1697
|
-
async function locateSession(s, locateBtn) {
|
|
1698
|
-
if (locateBtn) {
|
|
1699
|
-
locateBtn.disabled = true;
|
|
1700
|
-
locateBtn.textContent = t('sessions.locating');
|
|
1701
|
-
}
|
|
1702
|
-
try {
|
|
1703
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/locate`, { method: 'POST' });
|
|
1704
|
-
const body = await r.json();
|
|
1705
|
-
if (body.ok) {
|
|
1706
|
-
if (!locateBtn)
|
|
1707
|
-
return;
|
|
1708
|
-
let left = 30;
|
|
1709
|
-
locateBtn.textContent = t('sessions.cooldown', { seconds: left });
|
|
1710
|
-
const tick = setInterval(() => {
|
|
1711
|
-
left -= 1;
|
|
1712
|
-
if (left <= 0) {
|
|
1713
|
-
clearInterval(tick);
|
|
1714
|
-
locateBtn.disabled = false;
|
|
1715
|
-
locateBtn.textContent = t('sessions.locate');
|
|
1716
|
-
}
|
|
1717
|
-
else {
|
|
1718
|
-
locateBtn.textContent = t('sessions.cooldown', { seconds: left });
|
|
1719
|
-
}
|
|
1720
|
-
}, 1000);
|
|
1721
|
-
}
|
|
1722
|
-
else {
|
|
1723
|
-
alert(`Locate failed: ${body.error ?? r.status}`);
|
|
1724
|
-
if (locateBtn) {
|
|
1725
|
-
locateBtn.disabled = false;
|
|
1726
|
-
locateBtn.textContent = t('sessions.locate');
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
catch (e) {
|
|
1731
|
-
alert(`Locate error: ${e}`);
|
|
1732
|
-
if (locateBtn) {
|
|
1733
|
-
locateBtn.disabled = false;
|
|
1734
|
-
locateBtn.textContent = t('sessions.locate');
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
async function closeSession(s, closeBtn) {
|
|
1739
|
-
if (!confirm(t('sessions.closeConfirm')))
|
|
1740
|
-
return false;
|
|
1741
|
-
if (closeBtn)
|
|
1742
|
-
closeBtn.disabled = true;
|
|
1743
|
-
try {
|
|
1744
|
-
// 与批量关闭同口径:401(只读访客)/ 5xx 都不能当成功——否则抽屉
|
|
1745
|
-
// 静默关闭、board 重绘,用户以为关掉了。401 的提示由全局 fetch
|
|
1746
|
-
// patch 弹只读 toast,这里只负责不误报成功。
|
|
1747
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/close`, { method: 'POST' });
|
|
1748
|
-
const body = await r.json().catch(() => ({}));
|
|
1749
|
-
if (!r.ok || body?.ok === false) {
|
|
1750
|
-
if (r.status !== 401)
|
|
1751
|
-
alert(`Close failed: ${body?.error ?? r.status}`);
|
|
1752
|
-
return false;
|
|
1753
|
-
}
|
|
1754
|
-
return true;
|
|
1755
|
-
}
|
|
1756
|
-
catch (e) {
|
|
1757
|
-
alert(`Close error: ${e}`);
|
|
1758
|
-
return false;
|
|
1759
|
-
}
|
|
1760
|
-
finally {
|
|
1761
|
-
if (closeBtn)
|
|
1762
|
-
closeBtn.disabled = false;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
function invalidateSessionViews() {
|
|
1766
|
-
lastBoardHtml = '';
|
|
1767
|
-
lastTableHtml = '';
|
|
1768
|
-
lastKanbanHtml = '';
|
|
1769
|
-
}
|
|
1770
|
-
async function setSessionLocked(s, locked, btn) {
|
|
1771
|
-
const prev = !!s.locked;
|
|
1772
|
-
if (prev === locked)
|
|
1773
|
-
return true;
|
|
1774
|
-
s.locked = locked;
|
|
1775
|
-
invalidateSessionViews();
|
|
1776
|
-
rerender();
|
|
1777
|
-
if (btn)
|
|
1778
|
-
btn.disabled = true;
|
|
1779
|
-
try {
|
|
1780
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/lock`, {
|
|
1781
|
-
method: 'POST',
|
|
1782
|
-
headers: { 'content-type': 'application/json' },
|
|
1783
|
-
body: JSON.stringify({ locked }),
|
|
1784
|
-
});
|
|
1785
|
-
const body = await r.json().catch(() => ({}));
|
|
1786
|
-
if (!r.ok || body?.ok === false) {
|
|
1787
|
-
s.locked = prev;
|
|
1788
|
-
invalidateSessionViews();
|
|
1789
|
-
rerender();
|
|
1790
|
-
if (r.status !== 401)
|
|
1791
|
-
alert(`${t('sessions.lockFailed')}: ${body?.error ?? r.status}`);
|
|
1792
|
-
return false;
|
|
1793
|
-
}
|
|
1794
|
-
s.locked = !!body.locked;
|
|
1795
|
-
invalidateSessionViews();
|
|
1796
|
-
rerender();
|
|
1797
|
-
return true;
|
|
1798
|
-
}
|
|
1799
|
-
catch (e) {
|
|
1800
|
-
s.locked = prev;
|
|
1801
|
-
invalidateSessionViews();
|
|
1802
|
-
rerender();
|
|
1803
|
-
alert(`${t('sessions.lockFailed')}: ${e}`);
|
|
1804
|
-
return false;
|
|
1805
|
-
}
|
|
1806
|
-
finally {
|
|
1807
|
-
if (btn)
|
|
1808
|
-
btn.disabled = false;
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
// Per-session restart cooldown. The route returns 200 the instant the IPC is
|
|
1812
|
-
// sent — long before the worker's ~500ms+ respawn — so re-enabling the button
|
|
1813
|
-
// immediately let a second restart land inside the respawn window, which trips
|
|
1814
|
-
// the worker's tier-2 crash-loop guard (2 restarts before the CLI reaches its
|
|
1815
|
-
// prompt) and silently drops --resume, losing conversation context. This Set
|
|
1816
|
-
// debounces independently of the button DOM, which the table/board/kanban
|
|
1817
|
-
// rebuild via innerHTML on every SSE update (a DOM-only `disabled` is wiped).
|
|
1818
|
-
const restartCooldownIds = new Set();
|
|
1819
|
-
async function restartSession(s, restartBtn) {
|
|
1820
|
-
if (restartCooldownIds.has(s.sessionId))
|
|
1821
|
-
return false;
|
|
1822
|
-
if (!confirm(restartConfirmMessage(s)))
|
|
1823
|
-
return false;
|
|
1824
|
-
if (restartBtn)
|
|
1825
|
-
restartBtn.disabled = true;
|
|
1826
|
-
try {
|
|
1827
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/restart`, { method: 'POST' });
|
|
1828
|
-
const body = await r.json().catch(() => ({}));
|
|
1829
|
-
if (!r.ok || body?.ok === false) {
|
|
1830
|
-
if (r.status !== 401)
|
|
1831
|
-
alert(`${t('sessions.restartFailed')}: ${body?.error ?? r.status}`);
|
|
1832
|
-
return false;
|
|
1833
|
-
}
|
|
1834
|
-
restartCooldownIds.add(s.sessionId);
|
|
1835
|
-
setTimeout(() => restartCooldownIds.delete(s.sessionId), 5000);
|
|
1836
|
-
return true;
|
|
1837
|
-
}
|
|
1838
|
-
catch (e) {
|
|
1839
|
-
alert(`${t('sessions.restartFailed')}: ${e}`);
|
|
1840
|
-
return false;
|
|
1841
|
-
}
|
|
1842
|
-
finally {
|
|
1843
|
-
if (restartBtn)
|
|
1844
|
-
restartBtn.disabled = false;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
// ── Insight (owner-only): render a SafeInsightReport into the drawer. All
|
|
1848
|
-
// free text (suggestions, span input/output summaries) is escapeHtml'd —
|
|
1849
|
-
// the report is already fail-closed redacted upstream, this is defense in
|
|
1850
|
-
// depth against transcript content reaching innerHTML. ──────────────────
|
|
1851
|
-
function insightDur(ms) {
|
|
1852
|
-
if (ms === undefined)
|
|
1853
|
-
return '—';
|
|
1854
|
-
if (ms < 1000)
|
|
1855
|
-
return `${ms}ms`;
|
|
1856
|
-
if (ms < 60000)
|
|
1857
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
1858
|
-
return `${Math.floor(ms / 60000)}m${Math.round((ms % 60000) / 1000)}s`;
|
|
1859
|
-
}
|
|
1860
|
-
function renderInsightReport(rep) {
|
|
1861
|
-
if (!rep || (rep.status !== 'ok')) {
|
|
1862
|
-
const msg = rep?.error?.message ? escapeHtml(String(rep.error.message)) : escapeHtml(String(rep?.status ?? 'error'));
|
|
1863
|
-
return `<p>${t('sessions.insightUnavailable')}: ${msg}</p>`;
|
|
1864
|
-
}
|
|
1865
|
-
const a = rep.agg ?? {};
|
|
1866
|
-
if (!a.totalSpans)
|
|
1867
|
-
return `<p>${t('sessions.insightEmpty')}</p>`;
|
|
1868
|
-
const mut = 'color:var(--muted,#8f959e)';
|
|
1869
|
-
const parts = [];
|
|
1870
|
-
const metaBits = [];
|
|
1871
|
-
if (rep.meta?.asOf)
|
|
1872
|
-
metaBits.push(escapeHtml(t('sessions.insightAsOf', { asOf: String(rep.meta.asOf) })));
|
|
1873
|
-
if (rep.meta?.partial)
|
|
1874
|
-
metaBits.push(escapeHtml(t('sessions.insightPartial')));
|
|
1875
|
-
if (metaBits.length)
|
|
1876
|
-
parts.push(`<p style="font-size:12px;${mut}">${metaBits.join(' · ')}</p>`);
|
|
1877
|
-
parts.push(`<p>${escapeHtml(t('sessions.insightMetrics', {
|
|
1878
|
-
total: String(a.totalSpans ?? 0),
|
|
1879
|
-
failed: String(a.failedSpans ?? 0),
|
|
1880
|
-
slow: String(a.slowSpans ?? 0),
|
|
1881
|
-
rw: (a.readWriteRatio === null || a.readWriteRatio === undefined) ? '—' : String(a.readWriteRatio),
|
|
1882
|
-
compactions: String(a.compactions ?? 0),
|
|
1883
|
-
}))}</p>`);
|
|
1884
|
-
if (Array.isArray(rep.suggestions) && rep.suggestions.length) {
|
|
1885
|
-
const icon = (sev) => (sev === 'bad' ? '🔴' : sev === 'warn' ? '🟡' : 'ℹ️');
|
|
1886
|
-
const lis = rep.suggestions.map((sg) => `<li>${icon(sg.severity)} <b>${escapeHtml(String(sg.title ?? ''))}</b> — ${escapeHtml(String(sg.action ?? ''))}`
|
|
1887
|
-
+ (Array.isArray(sg.evidence) && sg.evidence.length ? `<br><small style="${mut}">${escapeHtml(sg.evidence.join(';'))}</small>` : '')
|
|
1888
|
-
+ `</li>`).join('');
|
|
1889
|
-
parts.push(`<details open><summary>${t('sessions.insightSuggestions')}</summary><ul style="padding-left:18px;margin:6px 0">${lis}</ul></details>`);
|
|
1890
|
-
}
|
|
1891
|
-
if (Array.isArray(rep.spans) && rep.spans.length) {
|
|
1892
|
-
const sIcon = (st) => (st === 'error' ? '🔴' : st === 'running' ? '⏳' : '✅');
|
|
1893
|
-
const rows = [...rep.spans].sort((x, y) => (x.relStartMs ?? 0) - (y.relStartMs ?? 0)).map((sp) => {
|
|
1894
|
-
const io = [sp.inputSummary, sp.outputSummary].filter(Boolean).map((x) => escapeHtml(String(x))).join(' → ');
|
|
1895
|
-
const cls = sp.status === 'error' ? ' style="color:var(--danger,#d33)"' : '';
|
|
1896
|
-
return `<tr${cls}><td>${sIcon(sp.status)}</td><td><code>${escapeHtml(String(sp.tool ?? ''))}</code></td><td>${escapeHtml(String(sp.phase ?? ''))}</td><td>${insightDur(sp.durationMs)}</td><td>#${escapeHtml(String(sp.turnIndex ?? 0))}</td><td>${io}</td></tr>`;
|
|
1897
|
-
}).join('');
|
|
1898
|
-
const cap = rep.meta?.capped
|
|
1899
|
-
? `<p style="font-size:12px;${mut}">${escapeHtml(t('sessions.insightCapped', { shown: String(rep.meta.spansReturned ?? rep.spans.length), total: String(rep.meta.spansTotal ?? rep.spans.length) }))}</p>`
|
|
1900
|
-
: '';
|
|
1901
|
-
parts.push(`<details><summary>${t('sessions.insightSpans')} (${rep.spans.length})</summary>${cap}<div style="max-height:320px;overflow:auto"><table style="width:100%;font-size:12px"><tbody>${rows}</tbody></table></div></details>`);
|
|
1902
|
-
}
|
|
1903
|
-
return parts.join('');
|
|
1904
|
-
}
|
|
1905
|
-
function openDrawer(s) {
|
|
1906
|
-
const closed = s.status === 'closed';
|
|
1907
|
-
const terminal = terminalHref(s);
|
|
1908
|
-
drawer.innerHTML = `<article>
|
|
1909
|
-
<header>
|
|
1910
|
-
<h3>${escapeHtml(stripMentionPrefix(s.title) || s.sessionId)}</h3>
|
|
1911
|
-
<span class="drawer-status-line">
|
|
1912
|
-
${statusBadgeHtml(s.status)}
|
|
1913
|
-
${lockChipHtml(s)}
|
|
1914
|
-
</span>
|
|
1915
|
-
<p><code>${escapeHtml(s.sessionId)}</code> <button data-copy="${escapeHtml(s.sessionId)}">${t('sessions.copy')}</button></p>
|
|
1916
|
-
</header>
|
|
1917
|
-
<p><b>${t('sessions.bot')}:</b> ${escapeHtml(botDisplayName(s))} · <b>${t('sessions.cli')}:</b> ${escapeHtml(s.cliId ?? '?')}</p>
|
|
1918
|
-
<p><b>${t('sessions.location')}:</b> ${escapeHtml(sessionLocationText(s))}</p>
|
|
1919
|
-
<p><b>chatId:</b> <code>${escapeHtml(s.chatId ?? '')}</code> <button data-copy="${escapeHtml(s.chatId ?? '')}">${t('sessions.copy')}</button></p>
|
|
1920
|
-
<p><b>rootMessageId:</b> <code>${escapeHtml(s.rootMessageId ?? '')}</code> <button data-copy="${escapeHtml(s.rootMessageId ?? '')}">${t('sessions.copy')}</button></p>
|
|
1921
|
-
${s.threadId ? `<p><b>threadId:</b> <code>${escapeHtml(s.threadId)}</code></p>` : ''}
|
|
1922
|
-
<p><b>${t('sessions.workingDir')}:</b> ${escapeHtml(s.workingDir ?? '-')}</p>
|
|
1923
|
-
<div class="actions">
|
|
1924
|
-
${chatScopeLink(s) ?? `<button id="locate-btn" type="button">${t('sessions.locate')}</button>`}
|
|
1925
|
-
<button id="history-drawer-btn" type="button">${t('sessions.history.title')}</button>
|
|
1926
|
-
${terminalControlsHtml(terminal)}
|
|
1927
|
-
${canRestartSession(s) ? `<button id="restart-btn" type="button">${t('sessions.restart')}</button>` : ''}
|
|
1928
|
-
${!closed ? `<button id="lock-btn" type="button">${escapeHtml(lockActionLabel(s))}</button>` : ''}
|
|
1929
|
-
${s.queued && !closed ? `<button id="start-btn" type="button" class="primary">${t('sessions.create.start')}</button>` : ''}
|
|
1930
|
-
${closed ? `<button id="resume-btn" type="button" class="primary">${t('sessions.resume')}</button>` : ''}
|
|
1931
|
-
${!closed ? `<button id="close-btn" type="button" class="contrast">${t('sessions.close')}</button>` : ''}
|
|
1932
|
-
<button id="land-btn" type="button">${t('sessions.land')}</button>
|
|
1933
|
-
${ui.authed ? `<button id="insight-btn" type="button">${t('sessions.insight')}</button>` : ''}
|
|
1934
|
-
</div>
|
|
1935
|
-
<div id="land-area"></div>
|
|
1936
|
-
<div id="insight-area"></div>
|
|
1937
|
-
<form method="dialog"><button>${t('sessions.dismiss')}</button></form>
|
|
1938
|
-
</article>`;
|
|
1939
|
-
drawer.querySelectorAll('[data-copy]').forEach(btn => {
|
|
1940
|
-
btn.onclick = () => {
|
|
1941
|
-
navigator.clipboard.writeText(btn.dataset.copy ?? '');
|
|
1942
|
-
btn.textContent = t('sessions.copied');
|
|
1943
|
-
setTimeout(() => { btn.textContent = t('sessions.copy'); }, 800);
|
|
1944
|
-
};
|
|
1945
|
-
});
|
|
1946
|
-
const locateBtn = drawer.querySelector('#locate-btn');
|
|
1947
|
-
if (locateBtn) {
|
|
1948
|
-
locateBtn.onclick = () => void locateSession(s, locateBtn);
|
|
1949
|
-
}
|
|
1950
|
-
const historyBtn = drawer.querySelector('#history-drawer-btn');
|
|
1951
|
-
if (historyBtn) {
|
|
1952
|
-
historyBtn.onclick = () => void openHistoryModal(s);
|
|
1953
|
-
}
|
|
1954
|
-
// Writable-terminal segment (.term-write) lives inside the drawer, outside
|
|
1955
|
-
// the board's click delegation — wire it directly.
|
|
1956
|
-
const writeBtn = drawer.querySelector('.term-write');
|
|
1957
|
-
if (writeBtn) {
|
|
1958
|
-
writeBtn.onclick = () => void openWriteLink(s, writeBtn);
|
|
1959
|
-
}
|
|
1960
|
-
const resumeBtn = drawer.querySelector('#resume-btn');
|
|
1961
|
-
if (resumeBtn) {
|
|
1962
|
-
resumeBtn.onclick = async () => {
|
|
1963
|
-
resumeBtn.disabled = true;
|
|
1964
|
-
try {
|
|
1965
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/resume`, { method: 'POST' });
|
|
1966
|
-
const body = await r.json().catch(() => ({}));
|
|
1967
|
-
if (!r.ok || body.ok === false) {
|
|
1968
|
-
alert(`${t('sessions.resumeFailed')}: ${body?.error ?? r.status}`);
|
|
1969
|
-
resumeBtn.disabled = false;
|
|
1970
|
-
return;
|
|
1971
|
-
}
|
|
1972
|
-
drawer.close();
|
|
1973
|
-
}
|
|
1974
|
-
catch (e) {
|
|
1975
|
-
alert(`${t('sessions.resumeFailed')}: ${e}`);
|
|
1976
|
-
resumeBtn.disabled = false;
|
|
1977
|
-
}
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
1980
|
-
const restartBtn = drawer.querySelector('#restart-btn');
|
|
1981
|
-
if (restartBtn) {
|
|
1982
|
-
restartBtn.onclick = async () => {
|
|
1983
|
-
if (await restartSession(s, restartBtn))
|
|
1984
|
-
drawer.close();
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
const closeBtn = drawer.querySelector('#close-btn');
|
|
1988
|
-
if (closeBtn) {
|
|
1989
|
-
closeBtn.onclick = async () => {
|
|
1990
|
-
if (await closeSession(s, closeBtn))
|
|
1991
|
-
drawer.close();
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
const lockBtn = drawer.querySelector('#lock-btn');
|
|
1995
|
-
if (lockBtn) {
|
|
1996
|
-
lockBtn.onclick = async () => {
|
|
1997
|
-
const next = !s.locked;
|
|
1998
|
-
if (await setSessionLocked(s, next, lockBtn)) {
|
|
1999
|
-
lockBtn.textContent = lockActionLabel(s);
|
|
2000
|
-
const line = drawer.querySelector('.drawer-status-line');
|
|
2001
|
-
if (line) {
|
|
2002
|
-
line.innerHTML = `${statusBadgeHtml(s.status)}${lockChipHtml(s)}`;
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
};
|
|
2006
|
-
}
|
|
2007
|
-
// 待办池会话「开始」:激活 parked 会话(发首轮、起 CLI)。
|
|
2008
|
-
const startBtn = drawer.querySelector('#start-btn');
|
|
2009
|
-
if (startBtn) {
|
|
2010
|
-
startBtn.onclick = async () => {
|
|
2011
|
-
startBtn.disabled = true;
|
|
2012
|
-
try {
|
|
2013
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/start`, { method: 'POST' });
|
|
2014
|
-
const body = await r.json().catch(() => ({}));
|
|
2015
|
-
if (!r.ok || body.ok === false) {
|
|
2016
|
-
if (r.status !== 401)
|
|
2017
|
-
alert(`${t('sessions.create.startFailed')}: ${body?.error ?? r.status}`);
|
|
2018
|
-
startBtn.disabled = false;
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
drawer.close();
|
|
2022
|
-
}
|
|
2023
|
-
catch (e) {
|
|
2024
|
-
alert(`${t('sessions.create.startFailed')}: ${e}`);
|
|
2025
|
-
startBtn.disabled = false;
|
|
2026
|
-
}
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
// Sandbox landing: fetch the clone's diff, show it, then apply/discard.
|
|
2030
|
-
const landBtn = drawer.querySelector('#land-btn');
|
|
2031
|
-
const landArea = drawer.querySelector('#land-area');
|
|
2032
|
-
if (landBtn && landArea) {
|
|
2033
|
-
landBtn.onclick = async () => {
|
|
2034
|
-
landBtn.disabled = true;
|
|
2035
|
-
landArea.innerHTML = `<p>${t('sessions.landLoading')}</p>`;
|
|
2036
|
-
try {
|
|
2037
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/sandbox-diff`);
|
|
2038
|
-
const d = await r.json().catch(() => ({}));
|
|
2039
|
-
if (!d.ok) {
|
|
2040
|
-
landArea.innerHTML = `<p>${t('sessions.landUnavailable')}: ${escapeHtml(d.error ?? String(r.status))}</p>`;
|
|
2041
|
-
landBtn.disabled = false;
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
|
-
if (d.empty) {
|
|
2045
|
-
landArea.innerHTML = `<p>${t('sessions.landEmpty')}</p>`;
|
|
2046
|
-
landBtn.disabled = false;
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
const full = String(d.patch ?? '');
|
|
2050
|
-
const patch = full.slice(0, 20000) + (full.length > 20000 ? '\n…(truncated)' : '');
|
|
2051
|
-
landArea.innerHTML = `
|
|
2052
|
-
<p><b>${d.files}</b> files (+${d.insertions}/-${d.deletions}) → <code>${escapeHtml(String(d.workingDir ?? ''))}</code></p>
|
|
2053
|
-
<pre style="max-height:320px;overflow:auto;white-space:pre-wrap">${escapeHtml(patch)}</pre>
|
|
2054
|
-
<div class="actions">
|
|
2055
|
-
<button id="land-apply" type="button" class="primary">${t('sessions.landApply')}</button>
|
|
2056
|
-
<button id="land-discard" type="button" class="contrast">${t('sessions.landDiscard')}</button>
|
|
2057
|
-
</div>`;
|
|
2058
|
-
const applyBtn = landArea.querySelector('#land-apply');
|
|
2059
|
-
const discardBtn = landArea.querySelector('#land-discard');
|
|
2060
|
-
applyBtn.onclick = async () => {
|
|
2061
|
-
applyBtn.disabled = true;
|
|
2062
|
-
discardBtn.disabled = true;
|
|
2063
|
-
const rr = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/sandbox-land/apply`, { method: 'POST' });
|
|
2064
|
-
const res = await rr.json().catch(() => ({}));
|
|
2065
|
-
landArea.innerHTML = res.ok
|
|
2066
|
-
? `<p>✅ ${t('sessions.landApplied')}: ${res.files} files (+${res.insertions}/-${res.deletions}) → <code>${escapeHtml(String(res.workingDir ?? ''))}</code></p>`
|
|
2067
|
-
: `<p>❌ ${t('sessions.landFailed')}: ${escapeHtml(res.error ?? String(rr.status))}</p>`;
|
|
2068
|
-
};
|
|
2069
|
-
discardBtn.onclick = async () => {
|
|
2070
|
-
await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/sandbox-land/discard`, { method: 'POST' });
|
|
2071
|
-
landArea.innerHTML = `<p>🗑 ${t('sessions.landDiscarded')}</p>`;
|
|
2072
|
-
};
|
|
2073
|
-
}
|
|
2074
|
-
catch (e) {
|
|
2075
|
-
landArea.innerHTML = `<p>${t('sessions.landUnavailable')}: ${escapeHtml(String(e))}</p>`;
|
|
2076
|
-
landBtn.disabled = false;
|
|
2077
|
-
}
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
// Insight (owner-only — button only rendered when ui.authed). Fetches the
|
|
2081
|
-
// SafeInsightReport with span detail and renders agg + suggestions + spans.
|
|
2082
|
-
const insightBtn = drawer.querySelector('#insight-btn');
|
|
2083
|
-
const insightArea = drawer.querySelector('#insight-area');
|
|
2084
|
-
if (insightBtn && insightArea) {
|
|
2085
|
-
insightBtn.onclick = async () => {
|
|
2086
|
-
insightBtn.disabled = true;
|
|
2087
|
-
insightArea.innerHTML = `<p>${t('sessions.insightLoading')}</p>`;
|
|
2088
|
-
try {
|
|
2089
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/insight?detail=spans`);
|
|
2090
|
-
const d = await r.json().catch(() => ({}));
|
|
2091
|
-
if (!d.ok || !d.report) {
|
|
2092
|
-
insightArea.innerHTML = `<p>${t('sessions.insightUnavailable')}: ${escapeHtml(String(d.error ?? r.status))}</p>`;
|
|
2093
|
-
}
|
|
2094
|
-
else {
|
|
2095
|
-
insightArea.innerHTML = renderInsightReport(d.report);
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
catch (e) {
|
|
2099
|
-
insightArea.innerHTML = `<p>${t('sessions.insightUnavailable')}: ${escapeHtml(String(e))}</p>`;
|
|
2100
|
-
}
|
|
2101
|
-
insightBtn.disabled = false;
|
|
2102
|
-
};
|
|
2103
|
-
}
|
|
2104
|
-
drawer.showModal();
|
|
2105
|
-
}
|
|
2106
|
-
tbody.addEventListener('click', e => {
|
|
2107
|
-
const target = e.target;
|
|
2108
|
-
if (target.classList.contains('row-select')) {
|
|
2109
|
-
const tr = target.closest('tr[data-id]');
|
|
2110
|
-
if (!tr)
|
|
2111
|
-
return;
|
|
2112
|
-
const cb = target;
|
|
2113
|
-
if (cb.checked)
|
|
2114
|
-
selected.add(tr.dataset.id);
|
|
2115
|
-
else
|
|
2116
|
-
selected.delete(tr.dataset.id);
|
|
2117
|
-
syncBulkUi(filtered());
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
const td = target.closest('td');
|
|
2121
|
-
if (td && td.querySelector('.row-select'))
|
|
2122
|
-
return;
|
|
2123
|
-
const tr = target.closest('tr[data-id]');
|
|
2124
|
-
if (!tr)
|
|
2125
|
-
return;
|
|
2126
|
-
const s = store.sessions.get(tr.dataset.id);
|
|
2127
|
-
if (s)
|
|
2128
|
-
openDrawer(s);
|
|
2129
|
-
});
|
|
2130
|
-
board.addEventListener('click', e => {
|
|
2131
|
-
const target = e.target;
|
|
2132
|
-
// 列头 ‹/› 按钮:交换列顺序(拖拽的键盘/触屏兜底)
|
|
2133
|
-
const moveBtn = target.closest('button[data-move-col]');
|
|
2134
|
-
if (moveBtn) {
|
|
2135
|
-
moveColumn(moveBtn.dataset.moveCol, Number(moveBtn.dataset.dir));
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
|
-
const card = target.closest('.session-card[data-id]');
|
|
2139
|
-
if (!card)
|
|
2140
|
-
return;
|
|
2141
|
-
const s = store.sessions.get(card.dataset.id);
|
|
2142
|
-
if (!s)
|
|
2143
|
-
return;
|
|
2144
|
-
const actionButton = target.closest('button[data-action]');
|
|
2145
|
-
if (actionButton) {
|
|
2146
|
-
const action = actionButton.dataset.action;
|
|
2147
|
-
if (action === 'details')
|
|
2148
|
-
openDrawer(s);
|
|
2149
|
-
else if (action === 'write-link')
|
|
2150
|
-
void openWriteLink(s, actionButton);
|
|
2151
|
-
else if (action === 'locate')
|
|
2152
|
-
void locateSession(s, actionButton);
|
|
2153
|
-
else if (action === 'restart')
|
|
2154
|
-
void restartSession(s, actionButton);
|
|
2155
|
-
else if (action === 'lock')
|
|
2156
|
-
void setSessionLocked(s, !s.locked, actionButton);
|
|
2157
|
-
else if (action === 'close')
|
|
2158
|
-
void closeSession(s, actionButton).then(ok => {
|
|
2159
|
-
if (ok) {
|
|
2160
|
-
selected.delete(s.sessionId);
|
|
2161
|
-
rerender();
|
|
2162
|
-
}
|
|
2163
|
-
});
|
|
2164
|
-
return;
|
|
2165
|
-
}
|
|
2166
|
-
// Clicking the card body toggles selection (the Feishu topic link lives
|
|
2167
|
-
// behind the 定位 button) — no checkbox, the whole card is the target.
|
|
2168
|
-
if (target.closest('a, button, input, label'))
|
|
2169
|
-
return;
|
|
2170
|
-
if (selected.has(s.sessionId))
|
|
2171
|
-
selected.delete(s.sessionId);
|
|
2172
|
-
else
|
|
2173
|
-
selected.add(s.sessionId);
|
|
2174
|
-
card.classList.toggle('selected', selected.has(s.sessionId));
|
|
2175
|
-
card.setAttribute('aria-pressed', String(selected.has(s.sessionId)));
|
|
2176
|
-
syncBulkUi(filtered().filter(r => r.status !== 'closed'));
|
|
2177
|
-
});
|
|
2178
|
-
// ── 列头拖拽排序:拖起一列,放到目标列的位置 ─────────────────────────────
|
|
2179
|
-
board.addEventListener('dragstart', e => {
|
|
2180
|
-
const header = e.target.closest('.session-board-column > header[draggable]');
|
|
2181
|
-
const col = header?.closest('.session-board-column');
|
|
2182
|
-
if (!col?.dataset.col)
|
|
2183
|
-
return;
|
|
2184
|
-
dragColId = col.dataset.col;
|
|
2185
|
-
col.classList.add('dragging');
|
|
2186
|
-
if (e.dataTransfer) {
|
|
2187
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
2188
|
-
e.dataTransfer.setData('text/plain', dragColId);
|
|
2189
|
-
}
|
|
2190
|
-
});
|
|
2191
|
-
board.addEventListener('dragover', e => {
|
|
2192
|
-
if (!dragColId)
|
|
2193
|
-
return;
|
|
2194
|
-
e.preventDefault();
|
|
2195
|
-
if (e.dataTransfer)
|
|
2196
|
-
e.dataTransfer.dropEffect = 'move';
|
|
2197
|
-
const target = e.target.closest('.session-board-column');
|
|
2198
|
-
board.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
|
2199
|
-
if (target && target.dataset.col !== dragColId)
|
|
2200
|
-
target.classList.add('drag-over');
|
|
2201
|
-
});
|
|
2202
|
-
board.addEventListener('drop', e => {
|
|
2203
|
-
if (!dragColId)
|
|
2204
|
-
return;
|
|
2205
|
-
e.preventDefault();
|
|
2206
|
-
const target = e.target.closest('.session-board-column');
|
|
2207
|
-
const from = dragColId;
|
|
2208
|
-
dragColId = null;
|
|
2209
|
-
if (target?.dataset.col)
|
|
2210
|
-
moveColumnTo(from, target.dataset.col);
|
|
2211
|
-
});
|
|
2212
|
-
board.addEventListener('dragend', () => {
|
|
2213
|
-
dragColId = null;
|
|
2214
|
-
board.querySelectorAll('.drag-over, .dragging').forEach(el => el.classList.remove('drag-over', 'dragging'));
|
|
2215
|
-
});
|
|
2216
|
-
viewButtons.forEach(btn => {
|
|
2217
|
-
btn.addEventListener('click', () => {
|
|
2218
|
-
const next = normalizeSessionsViewMode(btn.dataset.view) ?? 'board';
|
|
2219
|
-
if (next === viewMode)
|
|
2220
|
-
return;
|
|
2221
|
-
viewMode = next;
|
|
2222
|
-
writeStoredSessionsViewMode(window.localStorage, viewMode);
|
|
2223
|
-
rerender();
|
|
2224
|
-
});
|
|
2225
|
-
});
|
|
2226
|
-
monitorRoomOpenBtn.addEventListener('click', () => {
|
|
2227
|
-
window.location.href = monitorRoomUrl();
|
|
2228
|
-
});
|
|
2229
|
-
groupByBox.querySelectorAll('[data-groupby]').forEach(btn => {
|
|
2230
|
-
btn.addEventListener('click', () => {
|
|
2231
|
-
const raw = btn.dataset.groupby;
|
|
2232
|
-
const next = raw === 'bot' ? 'bot' : raw === 'team' ? 'team' : 'flow';
|
|
2233
|
-
if (next === kanbanGroupBy)
|
|
2234
|
-
return;
|
|
2235
|
-
kanbanGroupBy = next;
|
|
2236
|
-
writeStoredKanbanGroupBy(window.localStorage, next);
|
|
2237
|
-
lastKanbanHtml = '';
|
|
2238
|
-
rerender();
|
|
2239
|
-
});
|
|
2240
|
-
});
|
|
2241
|
-
teamSelect.addEventListener('change', () => {
|
|
2242
|
-
kanbanTeamKey = teamSelect.value;
|
|
2243
|
-
try {
|
|
2244
|
-
window.localStorage.setItem(KANBAN_TEAM_STORAGE_KEY, kanbanTeamKey);
|
|
2245
|
-
}
|
|
2246
|
-
catch { /* 仅当前页生效 */ }
|
|
2247
|
-
lastKanbanHtml = '';
|
|
2248
|
-
rerender();
|
|
2249
|
-
});
|
|
2250
|
-
// ── 看板交互:单击开终端弹窗(延迟仲裁让位双击)、铅笔/双击改标题、
|
|
2251
|
-
// 「详情」进抽屉、整卡拖拽换列与排序 ─────────────────────────────────────
|
|
2252
|
-
function cancelKanbanOpen() {
|
|
2253
|
-
if (kanbanOpenTimer !== null) {
|
|
2254
|
-
clearTimeout(kanbanOpenTimer);
|
|
2255
|
-
kanbanOpenTimer = null;
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
kanban.addEventListener('click', e => {
|
|
2259
|
-
const target = e.target;
|
|
2260
|
-
const card = target.closest('.kanban-card[data-id]');
|
|
2261
|
-
if (!card)
|
|
2262
|
-
return;
|
|
2263
|
-
const s = store.sessions.get(card.dataset.id);
|
|
2264
|
-
if (!s)
|
|
2265
|
-
return;
|
|
2266
|
-
const actionButton = target.closest('button[data-action]');
|
|
2267
|
-
if (actionButton) {
|
|
2268
|
-
if (actionButton.dataset.action === 'details')
|
|
2269
|
-
openDrawer(s);
|
|
2270
|
-
else if (actionButton.dataset.action === 'rename')
|
|
2271
|
-
startKanbanRename(card, s);
|
|
2272
|
-
else if (actionButton.dataset.action === 'history')
|
|
2273
|
-
void openHistoryModal(s);
|
|
2274
|
-
else if (actionButton.dataset.action === 'restart')
|
|
2275
|
-
void restartSession(s, actionButton);
|
|
2276
|
-
else if (actionButton.dataset.action === 'lock')
|
|
2277
|
-
void setSessionLocked(s, !s.locked, actionButton);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
if (target.closest('a, button, input, label'))
|
|
2281
|
-
return;
|
|
2282
|
-
cancelKanbanOpen();
|
|
2283
|
-
kanbanOpenTimer = setTimeout(() => {
|
|
2284
|
-
kanbanOpenTimer = null;
|
|
2285
|
-
void openTerminalModal(s);
|
|
2286
|
-
}, 220);
|
|
2287
|
-
});
|
|
2288
|
-
kanban.addEventListener('dblclick', e => {
|
|
2289
|
-
const target = e.target;
|
|
2290
|
-
const titleEl = target.closest('.kanban-card-title');
|
|
2291
|
-
const card = target.closest('.kanban-card[data-id]');
|
|
2292
|
-
if (!titleEl || !card)
|
|
2293
|
-
return;
|
|
2294
|
-
cancelKanbanOpen();
|
|
2295
|
-
const s = store.sessions.get(card.dataset.id);
|
|
2296
|
-
if (s)
|
|
2297
|
-
startKanbanRename(card, s);
|
|
2298
|
-
});
|
|
2299
|
-
kanban.addEventListener('keydown', e => {
|
|
2300
|
-
if (e.key !== 'Enter' && e.key !== ' ')
|
|
2301
|
-
return;
|
|
2302
|
-
const target = e.target;
|
|
2303
|
-
if (!target.classList?.contains('kanban-card'))
|
|
2304
|
-
return;
|
|
2305
|
-
e.preventDefault();
|
|
2306
|
-
const s = store.sessions.get(target.dataset.id);
|
|
2307
|
-
if (s)
|
|
2308
|
-
void openTerminalModal(s);
|
|
2309
|
-
});
|
|
2310
|
-
// ── 看板卡片拖拽 ──────────────────────────────────────────────────────────
|
|
2311
|
-
kanban.addEventListener('dragstart', e => {
|
|
2312
|
-
if (kanbanGroupBy === 'bot')
|
|
2313
|
-
return; // 机器人视角只读:会话不能拖给别的 bot
|
|
2314
|
-
const target = e.target;
|
|
2315
|
-
// 拖群组容器头部 = 整簇搬运
|
|
2316
|
-
const clusterHeader = target.closest('.kanban-cluster > header[draggable]');
|
|
2317
|
-
if (clusterHeader) {
|
|
2318
|
-
const cluster = clusterHeader.closest('.kanban-cluster');
|
|
2319
|
-
const col = cluster.closest('.kanban-column')?.dataset.col;
|
|
2320
|
-
if (!cluster.dataset.chat || !col)
|
|
2321
|
-
return;
|
|
2322
|
-
cancelKanbanOpen();
|
|
2323
|
-
kanbanDragClusterChat = cluster.dataset.chat;
|
|
2324
|
-
kanbanDragClusterCol = col;
|
|
2325
|
-
cluster.classList.add('dragging');
|
|
2326
|
-
if (e.dataTransfer) {
|
|
2327
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
2328
|
-
e.dataTransfer.setData('text/plain', `cluster:${kanbanDragClusterChat}`);
|
|
2329
|
-
}
|
|
2330
|
-
return;
|
|
2331
|
-
}
|
|
2332
|
-
const card = target.closest('.kanban-card[data-id]');
|
|
2333
|
-
if (!card)
|
|
2334
|
-
return;
|
|
2335
|
-
cancelKanbanOpen();
|
|
2336
|
-
kanbanDragId = card.dataset.id;
|
|
2337
|
-
card.classList.add('dragging');
|
|
2338
|
-
if (e.dataTransfer) {
|
|
2339
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
2340
|
-
e.dataTransfer.setData('text/plain', kanbanDragId);
|
|
2341
|
-
}
|
|
2342
|
-
});
|
|
2343
|
-
kanban.addEventListener('dragover', e => {
|
|
2344
|
-
if (!kanbanDragId && !kanbanDragClusterChat)
|
|
2345
|
-
return;
|
|
2346
|
-
e.preventDefault();
|
|
2347
|
-
if (e.dataTransfer)
|
|
2348
|
-
e.dataTransfer.dropEffect = 'move';
|
|
2349
|
-
const column = e.target.closest('.kanban-column');
|
|
2350
|
-
kanban.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
|
2351
|
-
kanban.querySelectorAll('.drop-before').forEach(el => el.classList.remove('drop-before'));
|
|
2352
|
-
if (!column)
|
|
2353
|
-
return;
|
|
2354
|
-
column.classList.add('drag-over');
|
|
2355
|
-
kanbanInsertBeforeCard(column, e.clientY)?.classList.add('drop-before');
|
|
2356
|
-
});
|
|
2357
|
-
kanban.addEventListener('drop', e => {
|
|
2358
|
-
const clusterChat = kanbanDragClusterChat;
|
|
2359
|
-
const clusterCol = kanbanDragClusterCol;
|
|
2360
|
-
const dragId = kanbanDragId;
|
|
2361
|
-
if (!dragId && !clusterChat)
|
|
2362
|
-
return;
|
|
2363
|
-
e.preventDefault();
|
|
2364
|
-
kanbanDragId = null;
|
|
2365
|
-
kanbanDragClusterChat = null;
|
|
2366
|
-
kanbanDragClusterCol = null;
|
|
2367
|
-
clearKanbanDragMarks();
|
|
2368
|
-
const column = e.target.closest('.kanban-column');
|
|
2369
|
-
const targetCol = column?.dataset.col;
|
|
2370
|
-
if (!column || !targetCol)
|
|
2371
|
-
return;
|
|
2372
|
-
const beforeCard = kanbanInsertBeforeCard(column, e.clientY);
|
|
2373
|
-
// ── 整簇搬运:源列里该群的全部卡片保持相对顺序插到落点 ──────────────────
|
|
2374
|
-
if (clusterChat && clusterCol) {
|
|
2375
|
-
const members = (lastKanbanGroups.get(clusterCol) ?? [])
|
|
2376
|
-
.filter((r) => String(r.chatId) === clusterChat)
|
|
2377
|
-
// 已关闭会话固定在「已完成」:整簇挪去别的列时留下它们
|
|
2378
|
-
.filter((r) => !(r.status === 'closed' && targetCol !== 'done'));
|
|
2379
|
-
if (!members.length)
|
|
2380
|
-
return;
|
|
2381
|
-
const memberIds = new Set(members.map((r) => r.sessionId));
|
|
2382
|
-
const colRows = (lastKanbanGroups.get(targetCol) ?? []).filter((r) => !memberIds.has(r.sessionId));
|
|
2383
|
-
let index = beforeCard ? colRows.findIndex((r) => r.sessionId === beforeCard.dataset.id) : colRows.length;
|
|
2384
|
-
if (index < 0)
|
|
2385
|
-
index = colRows.length;
|
|
2386
|
-
const prevRow = index > 0 ? colRows[index - 1] : null;
|
|
2387
|
-
const nextRow = index < colRows.length ? colRows[index] : null;
|
|
2388
|
-
const base = computeDropPosition(prevRow ? effectiveKanbanPosition(prevRow) : null, nextRow ? effectiveKanbanPosition(nextRow) : null);
|
|
2389
|
-
members.forEach((m, i) => {
|
|
2390
|
-
const pos = base + i * 0.001;
|
|
2391
|
-
if (kanbanGroupBy === 'team') {
|
|
2392
|
-
// 团队模式写 host 的共享编排,不动各会话的个人看板字段
|
|
2393
|
-
applyTeamBoardMove(String(m.sessionId), targetCol, pos);
|
|
2394
|
-
}
|
|
2395
|
-
else {
|
|
2396
|
-
const prev = { column: m.kanbanColumn, position: m.kanbanPosition };
|
|
2397
|
-
m.kanbanColumn = targetCol;
|
|
2398
|
-
m.kanbanPosition = pos;
|
|
2399
|
-
void persistBoardMove(m, targetCol, pos, prev);
|
|
2400
|
-
}
|
|
2401
|
-
});
|
|
2402
|
-
lastKanbanHtml = '';
|
|
2403
|
-
rerender();
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
// ── 单卡搬运 ─────────────────────────────────────────────────────────────
|
|
2407
|
-
// 对方部署的行不在 store 里——团队看板的远程缓存兜底
|
|
2408
|
-
const s = store.sessions.get(dragId) ?? kanbanRemoteRows.get(dragId);
|
|
2409
|
-
if (!s)
|
|
2410
|
-
return;
|
|
2411
|
-
// 已关闭会话固定在「已完成」列,只允许列内重排。
|
|
2412
|
-
if (s.status === 'closed' && targetCol !== 'done')
|
|
2413
|
-
return;
|
|
2414
|
-
const colRows = (lastKanbanGroups.get(targetCol) ?? []).filter((r) => r.sessionId !== dragId);
|
|
2415
|
-
let index = beforeCard ? colRows.findIndex((r) => r.sessionId === beforeCard.dataset.id) : colRows.length;
|
|
2416
|
-
if (index < 0)
|
|
2417
|
-
index = colRows.length;
|
|
2418
|
-
const prevRow = index > 0 ? colRows[index - 1] : null;
|
|
2419
|
-
const nextRow = index < colRows.length ? colRows[index] : null;
|
|
2420
|
-
const position = computeDropPosition(prevRow ? effectiveKanbanPosition(prevRow) : null, nextRow ? effectiveKanbanPosition(nextRow) : null);
|
|
2421
|
-
if (kanbanGroupBy === 'team') {
|
|
2422
|
-
applyTeamBoardMove(String(s.sessionId), targetCol, position);
|
|
2423
|
-
lastKanbanHtml = '';
|
|
2424
|
-
rerender();
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
const prev = { column: s.kanbanColumn, position: s.kanbanPosition };
|
|
2428
|
-
s.kanbanColumn = targetCol;
|
|
2429
|
-
s.kanbanPosition = position;
|
|
2430
|
-
lastKanbanHtml = '';
|
|
2431
|
-
rerender();
|
|
2432
|
-
void persistBoardMove(s, targetCol, position, prev);
|
|
2433
|
-
});
|
|
2434
|
-
kanban.addEventListener('dragend', () => {
|
|
2435
|
-
kanbanDragId = null;
|
|
2436
|
-
kanbanDragClusterChat = null;
|
|
2437
|
-
kanbanDragClusterCol = null;
|
|
2438
|
-
clearKanbanDragMarks();
|
|
2439
|
-
lastKanbanHtml = '';
|
|
2440
|
-
rerender();
|
|
2441
|
-
});
|
|
2442
|
-
// 点弹窗 backdrop 关闭;关闭时清空内容,立刻断开 iframe 里的终端 WebSocket。
|
|
2443
|
-
termModal.addEventListener('click', e => {
|
|
2444
|
-
if (e.target === termModal)
|
|
2445
|
-
termModal.close();
|
|
2446
|
-
});
|
|
2447
|
-
termModal.addEventListener('close', () => {
|
|
2448
|
-
termModal.innerHTML = '';
|
|
2449
|
-
});
|
|
2450
|
-
historyModal.addEventListener('click', e => {
|
|
2451
|
-
if (e.target === historyModal)
|
|
2452
|
-
historyModal.close();
|
|
2453
|
-
});
|
|
2454
|
-
historyModal.addEventListener('close', () => {
|
|
2455
|
-
historyModal.innerHTML = '';
|
|
2456
|
-
});
|
|
2457
|
-
selectAllBox.addEventListener('change', () => {
|
|
2458
|
-
const rows = filtered().filter(r => r.status !== 'closed');
|
|
2459
|
-
for (const row of rows) {
|
|
2460
|
-
if (selectAllBox.checked)
|
|
2461
|
-
selected.add(row.sessionId);
|
|
2462
|
-
else
|
|
2463
|
-
selected.delete(row.sessionId);
|
|
2464
|
-
}
|
|
2465
|
-
rerender();
|
|
2466
|
-
});
|
|
2467
|
-
bulkClearBtn.addEventListener('click', () => {
|
|
2468
|
-
selected.clear();
|
|
2469
|
-
rerender();
|
|
2470
|
-
});
|
|
2471
|
-
bulkMonitorRoomBtn.addEventListener('click', () => {
|
|
2472
|
-
const ids = [...selected].filter(id => !!store.sessions.get(id));
|
|
2473
|
-
const result = addMonitorRoomSessionIds(ids);
|
|
2474
|
-
const original = bulkMonitorRoomBtn.textContent;
|
|
2475
|
-
bulkMonitorRoomBtn.textContent = t('sessions.monitorRoomAdded', { added: result.added, total: result.total });
|
|
2476
|
-
window.setTimeout(() => {
|
|
2477
|
-
bulkMonitorRoomBtn.textContent = original;
|
|
2478
|
-
}, 1800);
|
|
2479
|
-
});
|
|
2480
|
-
bulkCloseBtn.addEventListener('click', async () => {
|
|
2481
|
-
const ids = [...selected];
|
|
2482
|
-
if (ids.length === 0)
|
|
2483
|
-
return;
|
|
2484
|
-
if (!confirm(t('sessions.closeBulkConfirm', { count: ids.length })))
|
|
2485
|
-
return;
|
|
2486
|
-
bulkCloseBtn.disabled = true;
|
|
2487
|
-
bulkClearBtn.disabled = true;
|
|
2488
|
-
const original = bulkCloseBtn.textContent;
|
|
2489
|
-
let done = 0;
|
|
2490
|
-
let failed = 0;
|
|
2491
|
-
const queue = [...ids];
|
|
2492
|
-
bulkCloseBtn.textContent = `0/${ids.length}`;
|
|
2493
|
-
async function worker() {
|
|
2494
|
-
while (queue.length) {
|
|
2495
|
-
const sid = queue.shift();
|
|
2496
|
-
try {
|
|
2497
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(sid)}/close`, { method: 'POST' });
|
|
2498
|
-
const body = await r.json().catch(() => ({}));
|
|
2499
|
-
if (!r.ok || body?.ok === false)
|
|
2500
|
-
failed += 1;
|
|
2501
|
-
}
|
|
2502
|
-
catch {
|
|
2503
|
-
failed += 1;
|
|
2504
|
-
}
|
|
2505
|
-
finally {
|
|
2506
|
-
done += 1;
|
|
2507
|
-
bulkCloseBtn.textContent = `${done}/${ids.length}`;
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
await Promise.all(Array.from({ length: Math.min(6, ids.length) }, () => worker()));
|
|
2512
|
-
bulkCloseBtn.textContent = original;
|
|
2513
|
-
bulkCloseBtn.disabled = false;
|
|
2514
|
-
bulkClearBtn.disabled = false;
|
|
2515
|
-
selected.clear();
|
|
2516
|
-
rerender();
|
|
2517
|
-
if (failed > 0)
|
|
2518
|
-
alert(`Failed: ${failed}/${ids.length}`);
|
|
2519
|
-
});
|
|
2520
|
-
async function setSelectedLocked(locked) {
|
|
2521
|
-
const rows = [...selected]
|
|
2522
|
-
.map(id => store.sessions.get(id))
|
|
2523
|
-
.filter((s) => !!s && s.status !== 'closed' && !!s.locked !== locked);
|
|
2524
|
-
if (rows.length === 0)
|
|
2525
|
-
return;
|
|
2526
|
-
bulkLockBtn.disabled = true;
|
|
2527
|
-
bulkUnlockBtn.disabled = true;
|
|
2528
|
-
const original = locked ? bulkLockBtn.textContent : bulkUnlockBtn.textContent;
|
|
2529
|
-
const activeBtn = locked ? bulkLockBtn : bulkUnlockBtn;
|
|
2530
|
-
let done = 0;
|
|
2531
|
-
let failed = 0;
|
|
2532
|
-
const queue = [...rows];
|
|
2533
|
-
activeBtn.textContent = `0/${rows.length}`;
|
|
2534
|
-
async function worker() {
|
|
2535
|
-
while (queue.length) {
|
|
2536
|
-
const s = queue.shift();
|
|
2537
|
-
const prev = !!s.locked;
|
|
2538
|
-
try {
|
|
2539
|
-
const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/lock`, {
|
|
2540
|
-
method: 'POST',
|
|
2541
|
-
headers: { 'content-type': 'application/json' },
|
|
2542
|
-
body: JSON.stringify({ locked }),
|
|
2543
|
-
});
|
|
2544
|
-
const body = await r.json().catch(() => ({}));
|
|
2545
|
-
if (!r.ok || body?.ok === false) {
|
|
2546
|
-
failed += 1;
|
|
2547
|
-
s.locked = prev;
|
|
2548
|
-
}
|
|
2549
|
-
else {
|
|
2550
|
-
s.locked = !!body.locked;
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
catch {
|
|
2554
|
-
failed += 1;
|
|
2555
|
-
s.locked = prev;
|
|
2556
|
-
}
|
|
2557
|
-
finally {
|
|
2558
|
-
done += 1;
|
|
2559
|
-
activeBtn.textContent = `${done}/${rows.length}`;
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
await Promise.all(Array.from({ length: Math.min(6, rows.length) }, () => worker()));
|
|
2564
|
-
activeBtn.textContent = original;
|
|
2565
|
-
invalidateSessionViews();
|
|
2566
|
-
rerender();
|
|
2567
|
-
if (failed > 0)
|
|
2568
|
-
alert(`${t('sessions.lockFailed')}: ${failed}/${rows.length}`);
|
|
2569
|
-
}
|
|
2570
|
-
bulkLockBtn.addEventListener('click', () => void setSelectedLocked(true));
|
|
2571
|
-
bulkUnlockBtn.addEventListener('click', () => void setSelectedLocked(false));
|
|
2572
|
-
idleCleanupThreshold.addEventListener('click', e => {
|
|
2573
|
-
// Don't let a threshold switch mid-cleanup repaint the count/pill against a
|
|
2574
|
-
// different threshold than the request that's already running.
|
|
2575
|
-
if (idleCleanupBusy)
|
|
2576
|
-
return;
|
|
2577
|
-
const btn = e.target?.closest('button[data-hours]');
|
|
2578
|
-
if (!btn)
|
|
2579
|
-
return;
|
|
2580
|
-
const next = parseIdleCleanupHours(btn.dataset.hours);
|
|
2581
|
-
if (!next || next === idleCleanupHours)
|
|
2582
|
-
return;
|
|
2583
|
-
idleCleanupHours = next;
|
|
2584
|
-
idleCleanupStatus.textContent = '';
|
|
2585
|
-
syncIdleCleanupUi();
|
|
2586
|
-
});
|
|
2587
|
-
idleCleanupBtn.addEventListener('click', async () => {
|
|
2588
|
-
const hours = selectedIdleCleanupHours();
|
|
2589
|
-
const candidates = currentIdleCleanupCandidates();
|
|
2590
|
-
if (candidates.length === 0)
|
|
2591
|
-
return;
|
|
2592
|
-
const label = idleCleanupLabel(hours);
|
|
2593
|
-
if (!confirm(t('sessions.idleCleanupConfirm', { count: candidates.length, threshold: label })))
|
|
2594
|
-
return;
|
|
2595
|
-
idleCleanupBusy = true;
|
|
2596
|
-
idleCleanupBtn.disabled = true;
|
|
2597
|
-
idleCleanupStatus.textContent = t('sessions.idleCleanupRunning');
|
|
2598
|
-
try {
|
|
2599
|
-
const r = await fetch('/api/sessions/cleanup-idle', {
|
|
2600
|
-
method: 'POST',
|
|
2601
|
-
headers: { 'content-type': 'application/json' },
|
|
2602
|
-
// Send the filtered candidate ids so the server closes exactly what the
|
|
2603
|
-
// operator saw and confirmed (it re-validates each is still idle).
|
|
2604
|
-
body: JSON.stringify({ olderThanHours: hours, sessionIds: candidates.map(c => c.sessionId) }),
|
|
2605
|
-
});
|
|
2606
|
-
const body = await r.json().catch(() => ({}));
|
|
2607
|
-
if (!r.ok) {
|
|
2608
|
-
if (r.status !== 401)
|
|
2609
|
-
alert(`${t('sessions.idleCleanupFailed')}: ${body?.error ?? r.status}`);
|
|
2610
|
-
idleCleanupStatus.textContent = '';
|
|
2611
|
-
return;
|
|
2612
|
-
}
|
|
2613
|
-
for (const item of body?.results ?? []) {
|
|
2614
|
-
if (item?.ok && item?.sessionId)
|
|
2615
|
-
selected.delete(String(item.sessionId));
|
|
2616
|
-
}
|
|
2617
|
-
idleCleanupStatus.textContent = t('sessions.idleCleanupDone', {
|
|
2618
|
-
closed: Number(body?.closed ?? 0),
|
|
2619
|
-
failed: Number(body?.failed ?? 0),
|
|
2620
|
-
});
|
|
2621
|
-
rerender();
|
|
2622
|
-
}
|
|
2623
|
-
catch (e) {
|
|
2624
|
-
alert(`${t('sessions.idleCleanupFailed')}: ${e}`);
|
|
2625
|
-
idleCleanupStatus.textContent = '';
|
|
2626
|
-
}
|
|
2627
|
-
finally {
|
|
2628
|
-
idleCleanupBusy = false;
|
|
2629
|
-
syncIdleCleanupUi();
|
|
2630
|
-
}
|
|
2631
|
-
});
|
|
2632
|
-
table.querySelectorAll('th[data-sort]').forEach(header => {
|
|
2633
|
-
header.addEventListener('click', () => {
|
|
2634
|
-
const key = header.dataset.sort;
|
|
2635
|
-
if (sortKey === key)
|
|
2636
|
-
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
2637
|
-
else {
|
|
2638
|
-
sortKey = key;
|
|
2639
|
-
sortDir = key === 'spawnedAt' || key === 'lastMessageAt' ? 'desc' : 'asc';
|
|
2640
|
-
}
|
|
2641
|
-
rerender();
|
|
2642
|
-
});
|
|
2643
|
-
});
|
|
2644
|
-
filtersForm.addEventListener('input', rerender);
|
|
2645
|
-
filtersForm.addEventListener('change', event => {
|
|
2646
|
-
const target = event.target;
|
|
2647
|
-
if (target?.name === 'showUnknownChats') {
|
|
2648
|
-
writeStoredSessionsShowUnknownChats(window.localStorage, target.checked);
|
|
2649
|
-
rerender();
|
|
2650
|
-
}
|
|
2651
|
-
});
|
|
2652
|
-
const unsubscribeStore = store.on(rerender);
|
|
2653
|
-
// 团队看板 30s 软刷新(拉对方部署的会话快照与共享编排);页面切走后
|
|
2654
|
-
// kanban 脱离 DOM,定时器自清。
|
|
2655
|
-
const teamBoardTimer = setInterval(() => {
|
|
2656
|
-
if (!document.body.contains(kanban)) {
|
|
2657
|
-
clearInterval(teamBoardTimer);
|
|
2658
|
-
return;
|
|
2659
|
-
}
|
|
2660
|
-
if (viewMode === 'kanban' && kanbanGroupBy === 'team') {
|
|
2661
|
-
lastKanbanHtml = '';
|
|
2662
|
-
rerender();
|
|
2663
|
-
}
|
|
2664
|
-
}, 30_000);
|
|
2665
|
-
rerender();
|
|
2666
|
-
// bot 友好名 / 群聊标题异步解析,回来后补一次重绘(首帧先显示原值)
|
|
2667
|
-
void loadNameMaps().then(rerender);
|
|
2668
|
-
return () => {
|
|
2669
|
-
unsubscribeStore();
|
|
2670
|
-
clearInterval(teamBoardTimer);
|
|
2671
|
-
cancelKanbanOpen();
|
|
2672
|
-
drawer.close();
|
|
2673
|
-
termModal.close();
|
|
2674
|
-
historyModal.close();
|
|
2675
|
-
// createSessionModal 现为全局元素(顶栏按钮拉起),不随本页卸载而关闭。
|
|
2676
|
-
};
|
|
2677
|
-
}
|
|
2678
196
|
//# sourceMappingURL=sessions.js.map
|