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,56 +1,144 @@
|
|
|
1
|
-
// Legacy (v0.2) workflow Run List / Detail pages — kept reachable only at the
|
|
2
|
-
// `#/legacy-workflow[/<runId>]` route so the v2 engine's Feishu progress /
|
|
3
|
-
// approval cards + daemon "Workflow started: Web:" links still open a working
|
|
4
|
-
// detail page. The main "工作流" nav now points at the v3 runs page (#/workflows);
|
|
5
|
-
// this page is intentionally out of the top nav.
|
|
6
|
-
//
|
|
7
|
-
// Polls /api/workflows/runs every 5s while visible. Each row links to
|
|
8
|
-
// #/legacy-workflow/<runId> — the Run Detail page (B path) hooks into the
|
|
9
|
-
// same hash route.
|
|
10
1
|
import { t } from './ui.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
export const LEGACY_WORKFLOW_POLL_MS = 5000;
|
|
3
|
+
export const LEGACY_WORKFLOW_DETAIL_POLL_MS = 2000;
|
|
4
|
+
export const LEGACY_WORKFLOW_TERMINAL_STATUSES = new Set(['succeeded', 'failed', 'cancelled']);
|
|
5
|
+
const RESUME_REQUIRES_CLI_SESSION_ID = new Set(['antigravity', 'codex-app', 'cursor', 'mira']);
|
|
6
|
+
const RESUME_USES_SESSION_ID = new Set([
|
|
7
|
+
'aiden',
|
|
8
|
+
'coco',
|
|
9
|
+
'claude-code',
|
|
10
|
+
'seed',
|
|
11
|
+
'relay',
|
|
12
|
+
'codex',
|
|
13
|
+
'mtr',
|
|
14
|
+
'hermes',
|
|
15
|
+
'pi',
|
|
16
|
+
'mir',
|
|
17
|
+
]);
|
|
18
|
+
export function parseLegacyWorkflowHash(hash) {
|
|
19
|
+
const detailMatch = hash.match(/^#\/legacy-workflow\/([^?#]+)(?:\?([^#]*))?$/);
|
|
20
|
+
if (!detailMatch)
|
|
21
|
+
return { kind: 'list' };
|
|
22
|
+
const params = new URLSearchParams(detailMatch[2] ?? '');
|
|
23
|
+
return {
|
|
24
|
+
kind: 'detail',
|
|
25
|
+
runId: decodeURIComponent(detailMatch[1]),
|
|
26
|
+
focusAttemptId: params.get('attempt') ?? undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function legacyWorkflowStatusFilters() {
|
|
30
|
+
return [
|
|
31
|
+
{ value: '', label: t('workflow.filter.nonTerminal') },
|
|
32
|
+
{ value: 'all', label: t('workflow.filter.all') },
|
|
33
|
+
{ value: 'pending', label: statusLabel('pending') },
|
|
34
|
+
{ value: 'running', label: statusLabel('running') },
|
|
35
|
+
{ value: 'waiting', label: statusLabel('waiting') },
|
|
36
|
+
{ value: 'succeeded', label: statusLabel('succeeded') },
|
|
37
|
+
{ value: 'failed', label: statusLabel('failed') },
|
|
38
|
+
{ value: 'cancelled', label: statusLabel('cancelled') },
|
|
21
39
|
];
|
|
22
|
-
return `
|
|
23
|
-
<nav class="wf-subnav">
|
|
24
|
-
<a href="#/legacy-workflow" class="active" data-i18n="workflow.subnav.runs">${escapeHtml(t('workflow.subnav.runs'))}</a>
|
|
25
|
-
<a href="#/workflows" data-i18n="workflow.subnav.v3">${escapeHtml(t('workflow.subnav.v3'))}</a>
|
|
26
|
-
</nav>
|
|
27
|
-
<form id="wf-filters" class="filters">
|
|
28
|
-
<input type="search" name="q" placeholder="${escapeHtml(t('workflow.searchPlaceholder'))}" />
|
|
29
|
-
<select name="status">
|
|
30
|
-
${statusOptions.map(([value, label]) => `<option value="${escapeHtml(value)}">${escapeHtml(label)}</option>`).join('')}
|
|
31
|
-
</select>
|
|
32
|
-
<span id="wf-last-load" class="muted"></span>
|
|
33
|
-
</form>
|
|
34
|
-
<table>
|
|
35
|
-
<thead><tr>
|
|
36
|
-
<th>${escapeHtml(t('workflow.table.run'))}</th><th>${escapeHtml(t('workflow.table.workflow'))}</th><th>${escapeHtml(t('workflow.table.status'))}</th>
|
|
37
|
-
<th>${escapeHtml(t('workflow.table.lastSeq'))}</th><th>${escapeHtml(t('workflow.table.dangling'))}</th><th>${escapeHtml(t('workflow.table.updated'))}</th>
|
|
38
|
-
<th>${escapeHtml(t('workflow.table.chatApp'))}</th>
|
|
39
|
-
</tr></thead>
|
|
40
|
-
<tbody id="wf-tbody"></tbody>
|
|
41
|
-
</table>
|
|
42
|
-
`;
|
|
43
40
|
}
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
export async function fetchLegacyWorkflowRuns(status, fetcher = fetch) {
|
|
42
|
+
const params = new URLSearchParams();
|
|
43
|
+
if (status === 'all')
|
|
44
|
+
params.set('all', '1');
|
|
45
|
+
else if (status)
|
|
46
|
+
params.set('status', status);
|
|
47
|
+
const response = await fetcher('/api/workflows/runs' + (params.toString() ? `?${params}` : ''));
|
|
48
|
+
if (!response.ok)
|
|
49
|
+
throw new Error(`HTTP ${response.status}`);
|
|
50
|
+
const body = await response.json();
|
|
51
|
+
return body.runs ?? [];
|
|
52
|
+
}
|
|
53
|
+
export async function fetchLegacyWorkflowSnapshot(runId, fetcher = fetch) {
|
|
54
|
+
const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/snapshot`);
|
|
55
|
+
if (response.status === 404)
|
|
56
|
+
throw new Error(t('workflow.detail.unknownRun'));
|
|
57
|
+
if (!response.ok)
|
|
58
|
+
throw new Error(t('workflow.detail.snapshotHttp', { status: response.status }));
|
|
59
|
+
return await response.json();
|
|
60
|
+
}
|
|
61
|
+
export async function fetchLegacyWorkflowEvents(runId, params, fetcher = fetch) {
|
|
62
|
+
const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/events?${params}`);
|
|
63
|
+
if (response.status === 404)
|
|
64
|
+
throw new Error(t('workflow.detail.unknownRun'));
|
|
65
|
+
if (!response.ok)
|
|
66
|
+
throw new Error(t('workflow.detail.eventsHttp', { status: response.status }));
|
|
67
|
+
return await response.json();
|
|
68
|
+
}
|
|
69
|
+
export async function cancelLegacyWorkflowRun(runId, fetcher = fetch) {
|
|
70
|
+
const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'content-type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ reason: 'cancelled via dashboard' }),
|
|
74
|
+
});
|
|
75
|
+
if (response.status === 401)
|
|
76
|
+
throw new Error(t('workflow.detail.writeAccessCancel'));
|
|
77
|
+
const body = await response.json().catch(() => ({}));
|
|
78
|
+
if (!response.ok || !body.ok) {
|
|
79
|
+
throw new Error(body.hint ?? body.error ?? t('workflow.detail.cancelHttp', { status: response.status }));
|
|
80
|
+
}
|
|
81
|
+
return body;
|
|
82
|
+
}
|
|
83
|
+
export async function resolveLegacyWorkflowWait(runId, action, comment, fetcher = fetch) {
|
|
84
|
+
const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/${action}`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'content-type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({ comment }),
|
|
88
|
+
});
|
|
89
|
+
if (response.status === 401)
|
|
90
|
+
throw new Error(t('workflow.detail.writeAccessApproval'));
|
|
91
|
+
const body = await response.json().catch(() => ({}));
|
|
92
|
+
if (!response.ok || !body.ok) {
|
|
93
|
+
throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.actionHttp', { action, status: response.status }));
|
|
94
|
+
}
|
|
95
|
+
return body;
|
|
96
|
+
}
|
|
97
|
+
export async function startLegacyWorkflowResumeSession(runId, activityId, attemptId, fetcher = fetch) {
|
|
98
|
+
const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}` +
|
|
99
|
+
`/attempts/${encodeURIComponent(activityId)}` +
|
|
100
|
+
`/${encodeURIComponent(attemptId)}/resume`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'content-type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({}),
|
|
104
|
+
});
|
|
105
|
+
if (response.status === 401)
|
|
106
|
+
throw new Error(t('workflow.detail.writeAccessResume'));
|
|
107
|
+
const body = await response.json().catch(() => ({}));
|
|
108
|
+
if (!response.ok || !body.ok || !body.resumeId || !body.url) {
|
|
109
|
+
throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeStartFailed', { status: response.status }));
|
|
110
|
+
}
|
|
111
|
+
return { resumeId: body.resumeId, url: body.url };
|
|
112
|
+
}
|
|
113
|
+
export async function endLegacyWorkflowResumeSession(runId, activityId, attemptId, fetcher = fetch) {
|
|
114
|
+
const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}` +
|
|
115
|
+
`/attempts/${encodeURIComponent(activityId)}` +
|
|
116
|
+
`/${encodeURIComponent(attemptId)}/resume/end`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'content-type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ reason: 'ended_by_dashboard' }),
|
|
120
|
+
});
|
|
121
|
+
if (response.status === 401)
|
|
122
|
+
throw new Error(t('workflow.detail.writeAccessResume'));
|
|
123
|
+
const body = await response.json().catch(() => ({}));
|
|
124
|
+
if (!response.ok || !body.ok) {
|
|
125
|
+
if (body.error === 'resume_not_running')
|
|
126
|
+
return { ended: false, resumeNotRunning: true };
|
|
127
|
+
throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeEndFailed', { status: response.status }));
|
|
128
|
+
}
|
|
129
|
+
return { ended: true, resumeNotRunning: false };
|
|
130
|
+
}
|
|
131
|
+
export function isTerminalWorkflowStatus(status) {
|
|
132
|
+
return LEGACY_WORKFLOW_TERMINAL_STATUSES.has(status);
|
|
49
133
|
}
|
|
50
|
-
function
|
|
134
|
+
export function statusLabel(status) {
|
|
135
|
+
const key = `workflow.status.${status}`;
|
|
136
|
+
const label = t(key);
|
|
137
|
+
return label === key ? status : label;
|
|
138
|
+
}
|
|
139
|
+
export function fmtUpdated(ms) {
|
|
51
140
|
const d = new Date(ms);
|
|
52
|
-
const
|
|
53
|
-
const diff = now - ms;
|
|
141
|
+
const diff = Date.now() - ms;
|
|
54
142
|
if (diff < 60_000)
|
|
55
143
|
return t('time.secondsAgo', { value: Math.max(1, Math.floor(diff / 1000)) });
|
|
56
144
|
if (diff < 3_600_000)
|
|
@@ -59,647 +147,7 @@ function fmtUpdated(ms) {
|
|
|
59
147
|
return t('time.hoursAgo', { value: Math.floor(diff / 3_600_000) });
|
|
60
148
|
return d.toISOString().slice(0, 19).replace('T', ' ');
|
|
61
149
|
}
|
|
62
|
-
function
|
|
63
|
-
const cls = TERMINAL.has(status) ? 'wf-status terminal' : 'wf-status live';
|
|
64
|
-
return `<span class="${cls} wf-status-${escapeHtml(status)}">${escapeHtml(statusLabel(status))}</span>`;
|
|
65
|
-
}
|
|
66
|
-
function statusLabel(status) {
|
|
67
|
-
const key = `workflow.status.${status}`;
|
|
68
|
-
const label = t(key);
|
|
69
|
-
return label === key ? status : label;
|
|
70
|
-
}
|
|
71
|
-
export function renderWorkflowsPage(root) {
|
|
72
|
-
const detailMatch = location.hash.match(/^#\/legacy-workflow\/([^?#]+)(?:\?([^#]*))?$/);
|
|
73
|
-
if (detailMatch) {
|
|
74
|
-
const params = new URLSearchParams(detailMatch[2] ?? '');
|
|
75
|
-
return renderWorkflowDetailPage(root, decodeURIComponent(detailMatch[1]), {
|
|
76
|
-
focusAttemptId: params.get('attempt') ?? undefined,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return renderWorkflowListPage(root);
|
|
80
|
-
}
|
|
81
|
-
function renderWorkflowListPage(root) {
|
|
82
|
-
root.innerHTML = pageHtml();
|
|
83
|
-
const tbody = root.querySelector('#wf-tbody');
|
|
84
|
-
const form = root.querySelector('#wf-filters');
|
|
85
|
-
const lastLoadEl = root.querySelector('#wf-last-load');
|
|
86
|
-
let cache = [];
|
|
87
|
-
let timer = null;
|
|
88
|
-
let inflight = false;
|
|
89
|
-
let lastErr = null;
|
|
90
|
-
let disposed = false;
|
|
91
|
-
function applyFilters(rows) {
|
|
92
|
-
const f = new FormData(form);
|
|
93
|
-
const q = (f.get('q') ?? '').trim().toLowerCase();
|
|
94
|
-
if (!q)
|
|
95
|
-
return rows;
|
|
96
|
-
return rows.filter((r) => r.runId.toLowerCase().includes(q) ||
|
|
97
|
-
r.workflowId.toLowerCase().includes(q) ||
|
|
98
|
-
(r.chatId ?? '').toLowerCase().includes(q));
|
|
99
|
-
}
|
|
100
|
-
function rerender() {
|
|
101
|
-
const rows = applyFilters(cache);
|
|
102
|
-
if (rows.length === 0) {
|
|
103
|
-
tbody.innerHTML = `<tr><td colspan="7" class="empty">${lastErr
|
|
104
|
-
? escapeHtml(t('workflow.list.failedLoad', { error: lastErr }))
|
|
105
|
-
: cache.length === 0
|
|
106
|
-
? escapeHtml(t('workflow.list.noRuns'))
|
|
107
|
-
: escapeHtml(t('workflow.list.noFilterMatch'))}</td></tr>`;
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
tbody.innerHTML = rows
|
|
111
|
-
.map((r) => {
|
|
112
|
-
const dangling = `${r.dEf}/${r.dAct}/${r.dWait}`;
|
|
113
|
-
const danglingCls = r.dEf + r.dAct + r.dWait > 0 ? 'wf-dangling has' : 'wf-dangling none';
|
|
114
|
-
const chatBits = [];
|
|
115
|
-
if (r.chatId)
|
|
116
|
-
chatBits.push(escapeHtml(r.chatId));
|
|
117
|
-
if (r.larkAppId)
|
|
118
|
-
chatBits.push(`<span class="muted">${escapeHtml(r.larkAppId)}</span>`);
|
|
119
|
-
const chatCell = chatBits.length > 0 ? chatBits.join('<br/>') : '—';
|
|
120
|
-
const errorSummary = renderRunErrorSummary(r);
|
|
121
|
-
return `<tr data-runid="${escapeHtml(r.runId)}">
|
|
122
|
-
<td><a href="#/legacy-workflow/${encodeURIComponent(r.runId)}"><code>${escapeHtml(r.runId)}</code></a></td>
|
|
123
|
-
<td>${escapeHtml(r.workflowId)}</td>
|
|
124
|
-
<td>${statusBadge(r.status)}${r.failedNodeId ? ` <span class="muted">(${escapeHtml(r.failedNodeId)})</span>` : ''}${errorSummary}</td>
|
|
125
|
-
<td>${r.lastSeq}</td>
|
|
126
|
-
<td class="${danglingCls}">${dangling}</td>
|
|
127
|
-
<td title="${escapeHtml(new Date(r.updatedAt).toISOString())}">${fmtUpdated(r.updatedAt)}</td>
|
|
128
|
-
<td>${chatCell}</td>
|
|
129
|
-
</tr>`;
|
|
130
|
-
})
|
|
131
|
-
.join('');
|
|
132
|
-
}
|
|
133
|
-
function setStatusLine() {
|
|
134
|
-
if (lastErr) {
|
|
135
|
-
lastLoadEl.textContent = t('workflow.list.error', { error: lastErr });
|
|
136
|
-
lastLoadEl.classList.add('error');
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
lastLoadEl.textContent = t('workflow.list.loaded', {
|
|
140
|
-
count: cache.length,
|
|
141
|
-
time: new Date().toLocaleTimeString(),
|
|
142
|
-
});
|
|
143
|
-
lastLoadEl.classList.remove('error');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async function poll() {
|
|
147
|
-
if (disposed || inflight)
|
|
148
|
-
return;
|
|
149
|
-
if (document.hidden)
|
|
150
|
-
return;
|
|
151
|
-
inflight = true;
|
|
152
|
-
try {
|
|
153
|
-
const status = form.elements.namedItem('status')?.value ?? '';
|
|
154
|
-
const params = new URLSearchParams();
|
|
155
|
-
if (status === 'all')
|
|
156
|
-
params.set('all', '1');
|
|
157
|
-
else if (status)
|
|
158
|
-
params.set('status', status);
|
|
159
|
-
const url = '/api/workflows/runs' + (params.toString() ? `?${params}` : '');
|
|
160
|
-
const r = await fetch(url);
|
|
161
|
-
if (!r.ok) {
|
|
162
|
-
lastErr = `HTTP ${r.status}`;
|
|
163
|
-
cache = [];
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
const body = (await r.json());
|
|
167
|
-
cache = body.runs ?? [];
|
|
168
|
-
lastErr = null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch (e) {
|
|
172
|
-
lastErr = e?.message ?? String(e);
|
|
173
|
-
cache = [];
|
|
174
|
-
}
|
|
175
|
-
finally {
|
|
176
|
-
inflight = false;
|
|
177
|
-
if (!disposed) {
|
|
178
|
-
rerender();
|
|
179
|
-
setStatusLine();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function scheduleNext() {
|
|
184
|
-
if (timer !== null)
|
|
185
|
-
window.clearTimeout(timer);
|
|
186
|
-
timer = window.setTimeout(async () => {
|
|
187
|
-
await poll();
|
|
188
|
-
if (!disposed)
|
|
189
|
-
scheduleNext();
|
|
190
|
-
}, POLL_MS);
|
|
191
|
-
}
|
|
192
|
-
function onVisibility() {
|
|
193
|
-
if (document.hidden)
|
|
194
|
-
return;
|
|
195
|
-
void poll();
|
|
196
|
-
}
|
|
197
|
-
form.addEventListener('input', () => {
|
|
198
|
-
rerender();
|
|
199
|
-
// Re-fetch immediately when status filter changes so the server-side
|
|
200
|
-
// filter applies; client-side `q` is row-local and doesn't need network.
|
|
201
|
-
});
|
|
202
|
-
form.addEventListener('change', (e) => {
|
|
203
|
-
if (e.target.getAttribute('name') === 'status') {
|
|
204
|
-
void poll();
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
document.addEventListener('visibilitychange', onVisibility);
|
|
208
|
-
// initial fetch + loop
|
|
209
|
-
void poll().then(() => {
|
|
210
|
-
if (!disposed)
|
|
211
|
-
scheduleNext();
|
|
212
|
-
});
|
|
213
|
-
// Cleanup hook — caller can dispose when navigating away.
|
|
214
|
-
return () => {
|
|
215
|
-
disposed = true;
|
|
216
|
-
if (timer !== null)
|
|
217
|
-
window.clearTimeout(timer);
|
|
218
|
-
document.removeEventListener('visibilitychange', onVisibility);
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
function renderWorkflowDetailPage(root, runId, opts = {}) {
|
|
222
|
-
root.innerHTML = `
|
|
223
|
-
<div class="wf-detail-head">
|
|
224
|
-
<a class="btn-link" href="#/legacy-workflow">${escapeHtml(t('workflow.detail.back'))}</a>
|
|
225
|
-
<div>
|
|
226
|
-
<h2><code>${escapeHtml(runId)}</code></h2>
|
|
227
|
-
<div id="wf-detail-subtitle" class="muted">${escapeHtml(t('workflow.detail.loading'))}</div>
|
|
228
|
-
</div>
|
|
229
|
-
<button id="wf-cancel-run" type="button" class="contrast" hidden>${escapeHtml(t('workflow.detail.cancel'))}</button>
|
|
230
|
-
<span id="wf-detail-refresh" class="muted"></span>
|
|
231
|
-
</div>
|
|
232
|
-
<section id="wf-detail-error" class="hint-warn" hidden></section>
|
|
233
|
-
<section id="wf-cancel-status" class="hint-ok" hidden></section>
|
|
234
|
-
<section id="wf-summary" class="wf-summary-grid"></section>
|
|
235
|
-
<section id="wf-dangling-panel"></section>
|
|
236
|
-
<section class="wf-panel">
|
|
237
|
-
<div class="wf-panel-title">
|
|
238
|
-
<h3>${escapeHtml(t('workflow.detail.parallel'))}</h3>
|
|
239
|
-
<span id="wf-parallel-meta" class="muted"></span>
|
|
240
|
-
</div>
|
|
241
|
-
<div id="wf-parallel-view"></div>
|
|
242
|
-
</section>
|
|
243
|
-
<section class="wf-panel">
|
|
244
|
-
<div class="wf-panel-title">
|
|
245
|
-
<h3>${escapeHtml(t('workflow.detail.nodes'))}</h3>
|
|
246
|
-
</div>
|
|
247
|
-
<div class="wf-table-scroll">
|
|
248
|
-
<table>
|
|
249
|
-
<thead><tr>
|
|
250
|
-
<th>${escapeHtml(t('workflow.detail.node'))}</th><th>${escapeHtml(t('workflow.detail.nodeStatus'))}</th><th>${escapeHtml(t('workflow.detail.activity'))}</th><th>${escapeHtml(t('workflow.detail.activityStatus'))}</th>
|
|
251
|
-
<th>${escapeHtml(t('workflow.detail.attempts'))}</th><th>${escapeHtml(t('workflow.detail.current'))}</th><th>${escapeHtml(t('workflow.detail.detail'))}</th>
|
|
252
|
-
</tr></thead>
|
|
253
|
-
<tbody id="wf-node-tbody"></tbody>
|
|
254
|
-
</table>
|
|
255
|
-
</div>
|
|
256
|
-
</section>
|
|
257
|
-
<section class="wf-panel">
|
|
258
|
-
<div class="wf-panel-title">
|
|
259
|
-
<h3>${escapeHtml(t('workflow.detail.nodeIO'))}</h3>
|
|
260
|
-
</div>
|
|
261
|
-
<div id="wf-io-list" class="wf-io-list"></div>
|
|
262
|
-
</section>
|
|
263
|
-
<section class="wf-panel">
|
|
264
|
-
<div class="wf-panel-title">
|
|
265
|
-
<h3>${escapeHtml(t('workflow.detail.timeline'))}</h3>
|
|
266
|
-
<button id="wf-load-older" type="button" hidden>${escapeHtml(t('workflow.detail.loadOlder'))}</button>
|
|
267
|
-
</div>
|
|
268
|
-
<div class="wf-table-scroll wf-timeline-scroll">
|
|
269
|
-
<table>
|
|
270
|
-
<thead><tr>
|
|
271
|
-
<th>${escapeHtml(t('workflow.detail.seq'))}</th><th>${escapeHtml(t('workflow.detail.event'))}</th><th>${escapeHtml(t('workflow.detail.actor'))}</th><th>${escapeHtml(t('workflow.detail.node'))}</th><th>${escapeHtml(t('workflow.detail.activity'))}</th><th>${escapeHtml(t('workflow.detail.error'))}</th><th>${escapeHtml(t('workflow.detail.time'))}</th>
|
|
272
|
-
</tr></thead>
|
|
273
|
-
<tbody id="wf-event-tbody"></tbody>
|
|
274
|
-
</table>
|
|
275
|
-
</div>
|
|
276
|
-
<div id="wf-event-meta" class="muted"></div>
|
|
277
|
-
</section>
|
|
278
|
-
`;
|
|
279
|
-
const subtitle = root.querySelector('#wf-detail-subtitle');
|
|
280
|
-
const refresh = root.querySelector('#wf-detail-refresh');
|
|
281
|
-
const errorEl = root.querySelector('#wf-detail-error');
|
|
282
|
-
const cancelStatusEl = root.querySelector('#wf-cancel-status');
|
|
283
|
-
const summaryEl = root.querySelector('#wf-summary');
|
|
284
|
-
const danglingEl = root.querySelector('#wf-dangling-panel');
|
|
285
|
-
const parallelEl = root.querySelector('#wf-parallel-view');
|
|
286
|
-
const parallelMeta = root.querySelector('#wf-parallel-meta');
|
|
287
|
-
const nodeTbody = root.querySelector('#wf-node-tbody');
|
|
288
|
-
const ioList = root.querySelector('#wf-io-list');
|
|
289
|
-
const timelineScroll = root.querySelector('.wf-timeline-scroll');
|
|
290
|
-
const eventTbody = root.querySelector('#wf-event-tbody');
|
|
291
|
-
const eventMeta = root.querySelector('#wf-event-meta');
|
|
292
|
-
const cancelBtn = root.querySelector('#wf-cancel-run');
|
|
293
|
-
const loadOlder = root.querySelector('#wf-load-older');
|
|
294
|
-
let snapshot = null;
|
|
295
|
-
let events = [];
|
|
296
|
-
let eventIds = new Set();
|
|
297
|
-
let oldestSeq = null;
|
|
298
|
-
let newestSeq = null;
|
|
299
|
-
let hasOlder = false;
|
|
300
|
-
let totalCount = 0;
|
|
301
|
-
let timer = null;
|
|
302
|
-
let disposed = false;
|
|
303
|
-
let inflight = false;
|
|
304
|
-
let canceling = false;
|
|
305
|
-
const openIOBlocks = new Set();
|
|
306
|
-
const ioScrollTops = new Map();
|
|
307
|
-
const approvalComments = new Map();
|
|
308
|
-
const approvalStatuses = new Map();
|
|
309
|
-
const resolvingWaits = new Set();
|
|
310
|
-
const resumeSessions = new Map();
|
|
311
|
-
const resumePending = new Set();
|
|
312
|
-
const resumeErrors = new Map();
|
|
313
|
-
// Per-card surgical update state. innerHTML-wiping the whole IO
|
|
314
|
-
// list every 2s polls causes terminal iframes inside to be torn off
|
|
315
|
-
// the document — the browser then discards the iframe's browsing
|
|
316
|
-
// context, and the next render reloads its src, which the user sees
|
|
317
|
-
// as flicker. Instead we keep each <article> stable across polls
|
|
318
|
-
// and only innerHTML-wipe the head/grid sub-containers; the
|
|
319
|
-
// terminal slot (with its iframe) is never detached. Keyed by
|
|
320
|
-
// activityId or nodeId so retries don't recreate the card.
|
|
321
|
-
const cardElements = new Map();
|
|
322
|
-
let timelineScrollTop = 0;
|
|
323
|
-
let focusAttemptId = opts.focusAttemptId;
|
|
324
|
-
function setError(message) {
|
|
325
|
-
if (!message) {
|
|
326
|
-
errorEl.hidden = true;
|
|
327
|
-
errorEl.textContent = '';
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
errorEl.hidden = false;
|
|
331
|
-
errorEl.textContent = message;
|
|
332
|
-
}
|
|
333
|
-
function setCancelStatus(message) {
|
|
334
|
-
if (!message) {
|
|
335
|
-
cancelStatusEl.hidden = true;
|
|
336
|
-
cancelStatusEl.textContent = '';
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
cancelStatusEl.hidden = false;
|
|
340
|
-
cancelStatusEl.textContent = message;
|
|
341
|
-
}
|
|
342
|
-
async function fetchSnapshot() {
|
|
343
|
-
const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/snapshot`);
|
|
344
|
-
if (res.status === 404) {
|
|
345
|
-
throw new Error(t('workflow.detail.unknownRun'));
|
|
346
|
-
}
|
|
347
|
-
if (!res.ok)
|
|
348
|
-
throw new Error(t('workflow.detail.snapshotHttp', { status: res.status }));
|
|
349
|
-
snapshot = (await res.json());
|
|
350
|
-
}
|
|
351
|
-
async function fetchEvents(params) {
|
|
352
|
-
const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/events?${params}`);
|
|
353
|
-
if (res.status === 404)
|
|
354
|
-
throw new Error(t('workflow.detail.unknownRun'));
|
|
355
|
-
if (!res.ok)
|
|
356
|
-
throw new Error(t('workflow.detail.eventsHttp', { status: res.status }));
|
|
357
|
-
return (await res.json());
|
|
358
|
-
}
|
|
359
|
-
function mergeEvents(incoming, direction) {
|
|
360
|
-
const fresh = incoming.filter((ev) => {
|
|
361
|
-
if (eventIds.has(ev.eventId))
|
|
362
|
-
return false;
|
|
363
|
-
eventIds.add(ev.eventId);
|
|
364
|
-
return true;
|
|
365
|
-
});
|
|
366
|
-
if (fresh.length === 0)
|
|
367
|
-
return;
|
|
368
|
-
events = direction === 'prepend' ? [...fresh, ...events] : [...events, ...fresh];
|
|
369
|
-
events.sort((a, b) => eventSeqFromId(a.eventId) - eventSeqFromId(b.eventId));
|
|
370
|
-
}
|
|
371
|
-
async function initialLoad() {
|
|
372
|
-
await fetchSnapshot();
|
|
373
|
-
const win = await fetchEvents(new URLSearchParams({ tail: '100' }));
|
|
374
|
-
events = [];
|
|
375
|
-
eventIds = new Set();
|
|
376
|
-
mergeEvents(win.events, 'append');
|
|
377
|
-
oldestSeq = win.oldestSeq;
|
|
378
|
-
newestSeq = win.newestSeq;
|
|
379
|
-
hasOlder = win.hasOlder;
|
|
380
|
-
totalCount = win.totalCount;
|
|
381
|
-
rerender();
|
|
382
|
-
}
|
|
383
|
-
async function poll() {
|
|
384
|
-
if (disposed || inflight || document.hidden)
|
|
385
|
-
return;
|
|
386
|
-
inflight = true;
|
|
387
|
-
try {
|
|
388
|
-
await fetchSnapshot();
|
|
389
|
-
if (newestSeq !== null) {
|
|
390
|
-
const win = await fetchEvents(new URLSearchParams({ afterSeq: String(newestSeq), limit: '200' }));
|
|
391
|
-
mergeEvents(win.events, 'append');
|
|
392
|
-
if (win.newestSeq !== null)
|
|
393
|
-
newestSeq = win.newestSeq;
|
|
394
|
-
if (oldestSeq === null && win.oldestSeq !== null)
|
|
395
|
-
oldestSeq = win.oldestSeq;
|
|
396
|
-
totalCount = win.totalCount;
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
const win = await fetchEvents(new URLSearchParams({ tail: '1' }));
|
|
400
|
-
mergeEvents(win.events, 'append');
|
|
401
|
-
oldestSeq = win.oldestSeq;
|
|
402
|
-
newestSeq = win.newestSeq;
|
|
403
|
-
hasOlder = win.hasOlder;
|
|
404
|
-
totalCount = win.totalCount;
|
|
405
|
-
}
|
|
406
|
-
setError(null);
|
|
407
|
-
rerender();
|
|
408
|
-
}
|
|
409
|
-
catch (err) {
|
|
410
|
-
setError(err?.message ?? String(err));
|
|
411
|
-
}
|
|
412
|
-
finally {
|
|
413
|
-
inflight = false;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
async function loadOlderEvents() {
|
|
417
|
-
if (oldestSeq === null || !hasOlder)
|
|
418
|
-
return;
|
|
419
|
-
loadOlder.disabled = true;
|
|
420
|
-
try {
|
|
421
|
-
const win = await fetchEvents(new URLSearchParams({ beforeSeq: String(oldestSeq), limit: '100' }));
|
|
422
|
-
mergeEvents(win.events, 'prepend');
|
|
423
|
-
if (win.oldestSeq !== null)
|
|
424
|
-
oldestSeq = win.oldestSeq;
|
|
425
|
-
hasOlder = win.hasOlder;
|
|
426
|
-
totalCount = win.totalCount;
|
|
427
|
-
setError(null);
|
|
428
|
-
rerender();
|
|
429
|
-
}
|
|
430
|
-
catch (err) {
|
|
431
|
-
setError(err?.message ?? String(err));
|
|
432
|
-
}
|
|
433
|
-
finally {
|
|
434
|
-
loadOlder.disabled = false;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
async function cancelRun() {
|
|
438
|
-
if (!snapshot || TERMINAL.has(snapshot.run.status) || canceling)
|
|
439
|
-
return;
|
|
440
|
-
if (!snapshot.chatBinding?.larkAppId) {
|
|
441
|
-
setError(t('workflow.detail.cancelUnavailable', { runId }));
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
const dangling = danglingSummary(snapshot);
|
|
445
|
-
const message = t('workflow.detail.cancelConfirm', { runId, ...dangling });
|
|
446
|
-
if (!window.confirm(message))
|
|
447
|
-
return;
|
|
448
|
-
canceling = true;
|
|
449
|
-
cancelBtn.disabled = true;
|
|
450
|
-
try {
|
|
451
|
-
const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
452
|
-
method: 'POST',
|
|
453
|
-
headers: { 'content-type': 'application/json' },
|
|
454
|
-
body: JSON.stringify({ reason: 'cancelled via dashboard' }),
|
|
455
|
-
});
|
|
456
|
-
if (res.status === 401) {
|
|
457
|
-
throw new Error(t('workflow.detail.writeAccessCancel'));
|
|
458
|
-
}
|
|
459
|
-
const body = (await res.json().catch(() => ({})));
|
|
460
|
-
if (!res.ok || !body.ok) {
|
|
461
|
-
throw new Error(body.hint ?? body.error ?? t('workflow.detail.cancelHttp', { status: res.status }));
|
|
462
|
-
}
|
|
463
|
-
setCancelStatus(body.pending ? t('workflow.detail.cancelPending') : null);
|
|
464
|
-
setError(null);
|
|
465
|
-
await poll();
|
|
466
|
-
}
|
|
467
|
-
catch (err) {
|
|
468
|
-
setError(err?.message ?? String(err));
|
|
469
|
-
}
|
|
470
|
-
finally {
|
|
471
|
-
canceling = false;
|
|
472
|
-
cancelBtn.disabled = false;
|
|
473
|
-
rerender();
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
async function startResumeSession(attemptId, activityId) {
|
|
477
|
-
if (resumePending.has(attemptId))
|
|
478
|
-
return;
|
|
479
|
-
resumePending.add(attemptId);
|
|
480
|
-
resumeErrors.delete(attemptId);
|
|
481
|
-
rerender();
|
|
482
|
-
try {
|
|
483
|
-
const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}` +
|
|
484
|
-
`/attempts/${encodeURIComponent(activityId)}` +
|
|
485
|
-
`/${encodeURIComponent(attemptId)}/resume`, {
|
|
486
|
-
method: 'POST',
|
|
487
|
-
headers: { 'content-type': 'application/json' },
|
|
488
|
-
body: JSON.stringify({}),
|
|
489
|
-
});
|
|
490
|
-
if (res.status === 401) {
|
|
491
|
-
throw new Error(t('workflow.detail.writeAccessResume'));
|
|
492
|
-
}
|
|
493
|
-
const body = (await res.json().catch(() => ({})));
|
|
494
|
-
if (!res.ok || !body.ok || !body.resumeId || !body.url) {
|
|
495
|
-
throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeStartFailed', { status: res.status }));
|
|
496
|
-
}
|
|
497
|
-
resumeSessions.set(attemptId, { resumeId: body.resumeId, url: body.url });
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
const message = err?.message ?? String(err);
|
|
501
|
-
resumeErrors.set(attemptId, message);
|
|
502
|
-
}
|
|
503
|
-
finally {
|
|
504
|
-
resumePending.delete(attemptId);
|
|
505
|
-
rerender();
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
async function endResumeSession(attemptId, activityId) {
|
|
509
|
-
if (resumePending.has(attemptId))
|
|
510
|
-
return;
|
|
511
|
-
resumePending.add(attemptId);
|
|
512
|
-
resumeErrors.delete(attemptId);
|
|
513
|
-
rerender();
|
|
514
|
-
try {
|
|
515
|
-
const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}` +
|
|
516
|
-
`/attempts/${encodeURIComponent(activityId)}` +
|
|
517
|
-
`/${encodeURIComponent(attemptId)}/resume/end`, {
|
|
518
|
-
method: 'POST',
|
|
519
|
-
headers: { 'content-type': 'application/json' },
|
|
520
|
-
body: JSON.stringify({ reason: 'ended_by_dashboard' }),
|
|
521
|
-
});
|
|
522
|
-
if (res.status === 401) {
|
|
523
|
-
throw new Error(t('workflow.detail.writeAccessResume'));
|
|
524
|
-
}
|
|
525
|
-
const body = (await res.json().catch(() => ({})));
|
|
526
|
-
if (!res.ok || !body.ok) {
|
|
527
|
-
// `resume_not_running` after a manual server-side end is benign —
|
|
528
|
-
// drop the local session so the iframe reverts to replay.
|
|
529
|
-
if (body.error === 'resume_not_running') {
|
|
530
|
-
resumeSessions.delete(attemptId);
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeEndFailed', { status: res.status }));
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
resumeSessions.delete(attemptId);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
catch (err) {
|
|
541
|
-
const message = err?.message ?? String(err);
|
|
542
|
-
resumeErrors.set(attemptId, message);
|
|
543
|
-
}
|
|
544
|
-
finally {
|
|
545
|
-
resumePending.delete(attemptId);
|
|
546
|
-
rerender();
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
async function resolveHumanGate(attemptId, action) {
|
|
550
|
-
if (resolvingWaits.has(attemptId))
|
|
551
|
-
return;
|
|
552
|
-
resolvingWaits.add(attemptId);
|
|
553
|
-
approvalStatuses.delete(attemptId);
|
|
554
|
-
rerender();
|
|
555
|
-
try {
|
|
556
|
-
const comment = approvalComments.get(attemptId)?.trim() || undefined;
|
|
557
|
-
const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/${action}`, {
|
|
558
|
-
method: 'POST',
|
|
559
|
-
headers: { 'content-type': 'application/json' },
|
|
560
|
-
body: JSON.stringify({ comment }),
|
|
561
|
-
});
|
|
562
|
-
if (res.status === 401) {
|
|
563
|
-
throw new Error(t('workflow.detail.writeAccessApproval'));
|
|
564
|
-
}
|
|
565
|
-
const body = (await res.json().catch(() => ({})));
|
|
566
|
-
if (!res.ok || !body.ok) {
|
|
567
|
-
throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.actionHttp', { action, status: res.status }));
|
|
568
|
-
}
|
|
569
|
-
const label = action === 'approve' ? t('workflow.detail.approved') : t('workflow.detail.rejected');
|
|
570
|
-
approvalStatuses.set(attemptId, {
|
|
571
|
-
kind: 'ok',
|
|
572
|
-
text: body.alreadyTerminal
|
|
573
|
-
? t('workflow.detail.alreadyTerminal', { label })
|
|
574
|
-
: body.pending
|
|
575
|
-
? t('workflow.detail.workflowContinue', { label })
|
|
576
|
-
: t('workflow.detail.workflowRefreshing', { label }),
|
|
577
|
-
});
|
|
578
|
-
setError(null);
|
|
579
|
-
await poll();
|
|
580
|
-
}
|
|
581
|
-
catch (err) {
|
|
582
|
-
const message = err?.message ?? String(err);
|
|
583
|
-
approvalStatuses.set(attemptId, { kind: 'error', text: message });
|
|
584
|
-
setError(message);
|
|
585
|
-
}
|
|
586
|
-
finally {
|
|
587
|
-
resolvingWaits.delete(attemptId);
|
|
588
|
-
rerender();
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
function rerender() {
|
|
592
|
-
if (!snapshot)
|
|
593
|
-
return;
|
|
594
|
-
timelineScrollTop = timelineScroll.scrollTop;
|
|
595
|
-
const run = snapshot.run;
|
|
596
|
-
if (TERMINAL.has(run.status))
|
|
597
|
-
setCancelStatus(null);
|
|
598
|
-
subtitle.innerHTML = `${escapeHtml(run.workflowId ?? '?')} · ${statusBadge(run.status)} · lastSeq ${snapshot.lastSeq}`;
|
|
599
|
-
refresh.textContent = t('workflow.detail.refreshed', { time: new Date().toLocaleTimeString() });
|
|
600
|
-
cancelBtn.hidden = TERMINAL.has(run.status);
|
|
601
|
-
cancelBtn.disabled = canceling || !snapshot.chatBinding?.larkAppId;
|
|
602
|
-
cancelBtn.textContent = snapshot.chatBinding?.larkAppId
|
|
603
|
-
? t('workflow.detail.cancel')
|
|
604
|
-
: t('workflow.detail.cliCancelOnly');
|
|
605
|
-
cancelBtn.title = snapshot.chatBinding?.larkAppId
|
|
606
|
-
? t('workflow.detail.cancelTitle')
|
|
607
|
-
: t('workflow.detail.cliCancelTitle', { runId });
|
|
608
|
-
renderSummary(summaryEl, snapshot);
|
|
609
|
-
renderDangling(danglingEl, snapshot);
|
|
610
|
-
renderParallelTimeline(parallelEl, parallelMeta, snapshot, events);
|
|
611
|
-
renderNodeActivityRows(nodeTbody, snapshot);
|
|
612
|
-
const focusConsumed = renderNodeIO(ioList, snapshot, openIOBlocks, ioScrollTops, {
|
|
613
|
-
comments: approvalComments,
|
|
614
|
-
statuses: approvalStatuses,
|
|
615
|
-
resolving: resolvingWaits,
|
|
616
|
-
onResolve: resolveHumanGate,
|
|
617
|
-
}, {
|
|
618
|
-
sessions: resumeSessions,
|
|
619
|
-
pending: resumePending,
|
|
620
|
-
errors: resumeErrors,
|
|
621
|
-
onStart: startResumeSession,
|
|
622
|
-
onEnd: endResumeSession,
|
|
623
|
-
}, focusAttemptId, cardElements);
|
|
624
|
-
if (focusConsumed)
|
|
625
|
-
focusAttemptId = undefined;
|
|
626
|
-
renderEvents(eventTbody, events);
|
|
627
|
-
timelineScroll.scrollTop = timelineScrollTop;
|
|
628
|
-
loadOlder.hidden = !hasOlder;
|
|
629
|
-
eventMeta.textContent = t('workflow.detail.eventsLoaded', { loaded: events.length, total: totalCount });
|
|
630
|
-
}
|
|
631
|
-
function scheduleNext() {
|
|
632
|
-
if (timer !== null)
|
|
633
|
-
window.clearTimeout(timer);
|
|
634
|
-
if (snapshot && TERMINAL.has(snapshot.run.status)) {
|
|
635
|
-
timer = null;
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
timer = window.setTimeout(async () => {
|
|
639
|
-
await poll();
|
|
640
|
-
if (!disposed)
|
|
641
|
-
scheduleNext();
|
|
642
|
-
}, DETAIL_POLL_MS);
|
|
643
|
-
}
|
|
644
|
-
function onVisibility() {
|
|
645
|
-
if (document.hidden)
|
|
646
|
-
return;
|
|
647
|
-
void poll().then(() => {
|
|
648
|
-
if (!disposed && timer === null)
|
|
649
|
-
scheduleNext();
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
loadOlder.addEventListener('click', () => void loadOlderEvents());
|
|
653
|
-
cancelBtn.addEventListener('click', () => void cancelRun());
|
|
654
|
-
document.addEventListener('visibilitychange', onVisibility);
|
|
655
|
-
void initialLoad()
|
|
656
|
-
.then(() => {
|
|
657
|
-
setError(null);
|
|
658
|
-
if (!disposed)
|
|
659
|
-
scheduleNext();
|
|
660
|
-
})
|
|
661
|
-
.catch((err) => {
|
|
662
|
-
setError(err?.message ?? String(err));
|
|
663
|
-
subtitle.textContent = t('workflow.detail.loadFailed');
|
|
664
|
-
});
|
|
665
|
-
return () => {
|
|
666
|
-
disposed = true;
|
|
667
|
-
if (timer !== null)
|
|
668
|
-
window.clearTimeout(timer);
|
|
669
|
-
document.removeEventListener('visibilitychange', onVisibility);
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
function renderSummary(el, snap) {
|
|
673
|
-
const r = snap.run;
|
|
674
|
-
const items = [
|
|
675
|
-
[t('workflow.summary.workflow'), escapeHtml(r.workflowId ?? '?')],
|
|
676
|
-
[t('workflow.summary.status'), statusBadge(r.status)],
|
|
677
|
-
[t('workflow.summary.lastSeq'), String(snap.lastSeq)],
|
|
678
|
-
[t('workflow.summary.updated'), escapeHtml(new Date(snap.updatedAt).toLocaleString())],
|
|
679
|
-
[t('workflow.summary.revision'), escapeHtml(short(r.revisionId))],
|
|
680
|
-
[t('workflow.summary.initiator'), escapeHtml(r.initiator ?? '-')],
|
|
681
|
-
];
|
|
682
|
-
if (r.failedNodeId)
|
|
683
|
-
items.push([t('workflow.summary.failedNode'), escapeHtml(r.failedNodeId)]);
|
|
684
|
-
if (r.cancelOriginEventId)
|
|
685
|
-
items.push([t('workflow.summary.cancelOrigin'), escapeHtml(r.cancelOriginEventId)]);
|
|
686
|
-
if (snap.chatBinding) {
|
|
687
|
-
items.push([t('workflow.summary.chat'), `<code>${escapeHtml(snap.chatBinding.chatId)}</code>`]);
|
|
688
|
-
items.push([t('workflow.summary.app'), `<code>${escapeHtml(snap.chatBinding.larkAppId)}</code>`]);
|
|
689
|
-
}
|
|
690
|
-
el.innerHTML = items
|
|
691
|
-
.map(([label, value]) => `<div class="wf-summary-item"><span>${label}</span><strong>${value}</strong></div>`)
|
|
692
|
-
.join('');
|
|
693
|
-
}
|
|
694
|
-
function renderRunErrorSummary(run) {
|
|
695
|
-
if (!run.errorCode)
|
|
696
|
-
return '';
|
|
697
|
-
const message = run.errorMessage ? ` — ${shortText(run.errorMessage, 96)}` : '';
|
|
698
|
-
return `<div class="wf-run-error">
|
|
699
|
-
<span class="muted error">${escapeHtml(run.errorCode)}</span>${escapeHtml(message)}
|
|
700
|
-
</div>`;
|
|
701
|
-
}
|
|
702
|
-
function danglingSummary(snap) {
|
|
150
|
+
export function danglingSummary(snap) {
|
|
703
151
|
const d = snap.dangling;
|
|
704
152
|
return {
|
|
705
153
|
total: new Set([
|
|
@@ -714,58 +162,7 @@ function danglingSummary(snap) {
|
|
|
714
162
|
cancels: d.cancels.length,
|
|
715
163
|
};
|
|
716
164
|
}
|
|
717
|
-
function
|
|
718
|
-
const d = snap.dangling;
|
|
719
|
-
const groups = [
|
|
720
|
-
[t('workflow.dangling.activities'), d.activities],
|
|
721
|
-
[t('workflow.dangling.effects'), d.effectAttempted],
|
|
722
|
-
[t('workflow.dangling.waits'), d.waits],
|
|
723
|
-
[t('workflow.dangling.cancels'), d.cancels],
|
|
724
|
-
];
|
|
725
|
-
const total = new Set(groups.flatMap(([, xs]) => xs)).size;
|
|
726
|
-
el.className = total > 0 ? 'wf-panel wf-dangling-panel has' : 'wf-panel wf-dangling-panel';
|
|
727
|
-
if (total === 0) {
|
|
728
|
-
el.innerHTML = `<div class="wf-panel-title"><h3>${escapeHtml(t('workflow.detail.dangling'))}</h3></div><div class="muted">${escapeHtml(t('workflow.detail.noDangling'))}</div>`;
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
el.innerHTML = `<div class="wf-panel-title"><h3>${escapeHtml(t('workflow.detail.dangling'))}</h3><span class="wf-dangling has">${total}</span></div>
|
|
732
|
-
<div class="wf-dangling-grid">
|
|
733
|
-
${groups
|
|
734
|
-
.map(([name, xs]) => `<div><strong>${name}</strong>${xs.length === 0
|
|
735
|
-
? `<div class="muted">${escapeHtml(t('workflow.detail.none'))}</div>`
|
|
736
|
-
: `<ul>${xs.map((x) => `<li><code>${escapeHtml(x)}</code></li>`).join('')}</ul>`}</div>`)
|
|
737
|
-
.join('')}
|
|
738
|
-
</div>`;
|
|
739
|
-
}
|
|
740
|
-
function renderParallelTimeline(el, metaEl, snap, events) {
|
|
741
|
-
const items = buildAttemptTimeline(events, snap);
|
|
742
|
-
if (items.length === 0) {
|
|
743
|
-
metaEl.textContent = '';
|
|
744
|
-
el.innerHTML = `<div class="empty">${escapeHtml(t('workflow.detail.noParallelData'))}</div>`;
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
const now = Date.now();
|
|
748
|
-
const start = Math.min(...items.map((item) => item.startedAt));
|
|
749
|
-
const end = Math.max(...items.map((item) => item.endedAt ?? now), start + 1000);
|
|
750
|
-
const duration = Math.max(1, end - start);
|
|
751
|
-
const maxParallel = maxConcurrency(items, now);
|
|
752
|
-
const running = items.filter((item) => !item.endedAt && (item.status === 'running' || item.status === 'effectAttempting')).length;
|
|
753
|
-
metaEl.textContent = t('workflow.detail.parallelMeta', {
|
|
754
|
-
count: items.length,
|
|
755
|
-
max: maxParallel,
|
|
756
|
-
running,
|
|
757
|
-
});
|
|
758
|
-
const rows = items
|
|
759
|
-
.sort((a, b) => a.startedAt - b.startedAt || a.activityId.localeCompare(b.activityId))
|
|
760
|
-
.map((item) => renderParallelRow(item, start, duration, now))
|
|
761
|
-
.join('');
|
|
762
|
-
el.innerHTML = `<div class="wf-parallel-axis">
|
|
763
|
-
<span title="${escapeHtml(new Date(start).toISOString())}">${escapeHtml(formatClock(start))}</span>
|
|
764
|
-
<span title="${escapeHtml(new Date(end).toISOString())}">${escapeHtml(formatClock(end))}</span>
|
|
765
|
-
</div>
|
|
766
|
-
<div class="wf-parallel-list">${rows}</div>`;
|
|
767
|
-
}
|
|
768
|
-
function buildAttemptTimeline(events, snap) {
|
|
165
|
+
export function buildAttemptTimeline(events, snap) {
|
|
769
166
|
const byAttempt = new Map();
|
|
770
167
|
const activityOwner = new Map(snap.activities.map((activity) => [activity.activityId, activity.ownerNodeId]));
|
|
771
168
|
for (const event of [...events].sort((a, b) => eventSeqFromId(a.eventId) - eventSeqFromId(b.eventId))) {
|
|
@@ -819,30 +216,7 @@ function buildAttemptTimeline(events, snap) {
|
|
|
819
216
|
}
|
|
820
217
|
return [...byAttempt.values()];
|
|
821
218
|
}
|
|
822
|
-
function
|
|
823
|
-
const end = item.endedAt ?? now;
|
|
824
|
-
const left = clamp(((item.startedAt - start) / duration) * 100, 0, 100);
|
|
825
|
-
const width = clamp(((Math.max(end, item.startedAt + 1) - item.startedAt) / duration) * 100, 0.7, 100 - left);
|
|
826
|
-
const label = item.nodeId ?? item.activityId;
|
|
827
|
-
const attempt = item.attemptNumber !== undefined ? `#${item.attemptNumber}` : short(item.attemptId);
|
|
828
|
-
const title = [
|
|
829
|
-
`${label} ${item.status}`,
|
|
830
|
-
`${new Date(item.startedAt).toISOString()} → ${item.endedAt ? new Date(item.endedAt).toISOString() : t('workflow.detail.parallelNow')}`,
|
|
831
|
-
item.endType ? `end: ${item.endType}` : undefined,
|
|
832
|
-
].filter(Boolean).join('\n');
|
|
833
|
-
return `<div class="wf-parallel-row">
|
|
834
|
-
<div class="wf-parallel-label">
|
|
835
|
-
<code>${escapeHtml(label)}</code>
|
|
836
|
-
<span class="muted">${escapeHtml(item.activityId)} · ${escapeHtml(attempt)}</span>
|
|
837
|
-
</div>
|
|
838
|
-
<div class="wf-parallel-track">
|
|
839
|
-
<div class="wf-parallel-bar wf-parallel-${escapeHtml(item.status)}" style="left:${left.toFixed(3)}%;width:${width.toFixed(3)}%;" title="${escapeHtml(title)}">
|
|
840
|
-
<span>${escapeHtml(statusLabel(item.status))}</span>
|
|
841
|
-
</div>
|
|
842
|
-
</div>
|
|
843
|
-
</div>`;
|
|
844
|
-
}
|
|
845
|
-
function maxConcurrency(items, now) {
|
|
219
|
+
export function maxConcurrency(items, now) {
|
|
846
220
|
const points = [];
|
|
847
221
|
for (const item of items) {
|
|
848
222
|
points.push({ time: item.startedAt, delta: 1 });
|
|
@@ -857,132 +231,13 @@ function maxConcurrency(items, now) {
|
|
|
857
231
|
}
|
|
858
232
|
return max;
|
|
859
233
|
}
|
|
860
|
-
function
|
|
861
|
-
|
|
862
|
-
type === 'activityFailed' ||
|
|
863
|
-
type === 'activityTimedOut' ||
|
|
864
|
-
type === 'activityCanceled';
|
|
865
|
-
}
|
|
866
|
-
function terminalStatusForEvent(type) {
|
|
867
|
-
if (type === 'activitySucceeded')
|
|
868
|
-
return 'succeeded';
|
|
869
|
-
if (type === 'activityCanceled')
|
|
870
|
-
return 'cancelled';
|
|
871
|
-
if (type === 'activityTimedOut')
|
|
872
|
-
return 'timedOut';
|
|
873
|
-
return 'failed';
|
|
874
|
-
}
|
|
875
|
-
function renderNodeActivityRows(tbody, snap) {
|
|
876
|
-
const byId = new Map(snap.activities.map((a) => [a.activityId, a]));
|
|
877
|
-
const used = new Set();
|
|
878
|
-
const rows = [];
|
|
879
|
-
for (const node of snap.nodes) {
|
|
880
|
-
const activity = (node.activityId ? byId.get(node.activityId) : undefined) ??
|
|
881
|
-
snap.activities.find((a) => a.ownerNodeId === node.nodeId);
|
|
882
|
-
if (activity)
|
|
883
|
-
used.add(activity.activityId);
|
|
884
|
-
rows.push(renderNodeActivityRow(node, activity));
|
|
885
|
-
}
|
|
886
|
-
for (const activity of snap.activities) {
|
|
887
|
-
if (used.has(activity.activityId))
|
|
888
|
-
continue;
|
|
889
|
-
rows.push(renderNodeActivityRow(undefined, activity));
|
|
890
|
-
}
|
|
891
|
-
tbody.innerHTML = rows.length > 0
|
|
892
|
-
? rows.join('')
|
|
893
|
-
: `<tr><td colspan="7" class="empty">${escapeHtml(t('workflow.detail.noNodes'))}</td></tr>`;
|
|
894
|
-
}
|
|
895
|
-
function renderNodeActivityRow(node, activity) {
|
|
896
|
-
const latest = activity?.attempts[activity.attempts.length - 1];
|
|
897
|
-
return `<tr>
|
|
898
|
-
<td>${node ? `<code>${escapeHtml(node.nodeId)}</code>` : '<span class="muted">-</span>'}</td>
|
|
899
|
-
<td>${node ? statusBadge(node.status) : '<span class="muted">-</span>'}</td>
|
|
900
|
-
<td>${activity ? `<code>${escapeHtml(activity.activityId)}</code>` : '<span class="muted">-</span>'}</td>
|
|
901
|
-
<td>${activity ? statusBadge(activity.status) : '<span class="muted">-</span>'}</td>
|
|
902
|
-
<td>${activity?.attempts.length ?? 0}</td>
|
|
903
|
-
<td>${latest ? `<code>${escapeHtml(latest.attemptId)}</code>` : '<span class="muted">-</span>'}</td>
|
|
904
|
-
<td>${latest ? renderAttemptDetail(latest) : `<span class="muted">${escapeHtml(t('workflow.detail.idle'))}</span>`}</td>
|
|
905
|
-
</tr>`;
|
|
906
|
-
}
|
|
907
|
-
function renderNodeIO(el, snap, openBlocks, scrollTops, approval, resume, focusAttemptId, cardElements) {
|
|
908
|
-
syncIOBlockState(el, openBlocks, scrollTops);
|
|
909
|
-
syncApprovalComments(el, approval.comments);
|
|
910
|
-
const focusHasTerminal = !!(focusAttemptId && snap.attemptIO?.[focusAttemptId]?.terminal);
|
|
911
|
-
if (focusHasTerminal && focusAttemptId) {
|
|
912
|
-
openBlocks.add(ioBlockKey(focusAttemptId, t('workflow.detail.liveTerminal')));
|
|
913
|
-
}
|
|
914
|
-
// Per-card surgical update — we never `innerHTML=`-wipe the parent
|
|
915
|
-
// `el` because doing so detaches every terminal iframe from the
|
|
916
|
-
// document, which in all major browsers discards the iframe's
|
|
917
|
-
// browsing context (the WebSocket included) and the next paint
|
|
918
|
-
// reloads the iframe → terminal flickers. Instead we keep each
|
|
919
|
-
// <article> stable across polls and only innerHTML-wipe the
|
|
920
|
-
// per-card head/grid sub-containers; the terminal-slot div (whose
|
|
921
|
-
// iframe holds the live WebSocket) is never detached.
|
|
922
|
-
const descriptors = buildCardDescriptors(snap);
|
|
923
|
-
const seenKeys = new Set();
|
|
924
|
-
if (cardElements) {
|
|
925
|
-
for (const desc of descriptors) {
|
|
926
|
-
seenKeys.add(desc.key);
|
|
927
|
-
let entry = cardElements.get(desc.key);
|
|
928
|
-
if (!entry) {
|
|
929
|
-
entry = createCardEntry(desc.key);
|
|
930
|
-
cardElements.set(desc.key, entry);
|
|
931
|
-
el.appendChild(entry.article);
|
|
932
|
-
}
|
|
933
|
-
updateCardEntry(entry, desc, openBlocks, approval, resume, focusAttemptId);
|
|
934
|
-
}
|
|
935
|
-
for (const [key, entry] of Array.from(cardElements)) {
|
|
936
|
-
if (seenKeys.has(key))
|
|
937
|
-
continue;
|
|
938
|
-
entry.article.remove();
|
|
939
|
-
cardElements.delete(key);
|
|
940
|
-
}
|
|
941
|
-
if (descriptors.length === 0) {
|
|
942
|
-
// Empty-state placeholder. Use a sentinel child rather than
|
|
943
|
-
// wiping innerHTML so any (hypothetically lingering) terminal
|
|
944
|
-
// iframe wouldn't be detached. Cards are already cleared via
|
|
945
|
-
// the loop above, so it's safe to add the placeholder here.
|
|
946
|
-
if (!el.querySelector('.wf-io-empty-placeholder')) {
|
|
947
|
-
const empty = document.createElement('div');
|
|
948
|
-
empty.className = 'empty wf-io-empty-placeholder';
|
|
949
|
-
empty.textContent = t('workflow.detail.noNodeIO');
|
|
950
|
-
el.appendChild(empty);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
else {
|
|
954
|
-
el.querySelector('.wf-io-empty-placeholder')?.remove();
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
// No cache provided (unit tests etc.): fall back to bulk innerHTML.
|
|
959
|
-
const cards = [];
|
|
960
|
-
for (const desc of descriptors) {
|
|
961
|
-
cards.push(renderIOCardHtml(desc, openBlocks, approval, resume, focusAttemptId));
|
|
962
|
-
}
|
|
963
|
-
el.innerHTML = cards.length > 0
|
|
964
|
-
? cards.join('')
|
|
965
|
-
: `<div class="empty">${escapeHtml(t('workflow.detail.noNodeIO'))}</div>`;
|
|
966
|
-
}
|
|
967
|
-
restoreIOBlockScroll(el, scrollTops);
|
|
968
|
-
const focusVisible = scrollFocusedAttemptIntoView(el, focusAttemptId);
|
|
969
|
-
attachIOBlockToggleTracking(el, openBlocks);
|
|
970
|
-
attachIOBlockScrollTracking(el, scrollTops);
|
|
971
|
-
attachApprovalControls(el, approval);
|
|
972
|
-
attachResumeControls(el, resume);
|
|
973
|
-
// Keep the deeplink focus active until terminal.json is visible. The
|
|
974
|
-
// progress card link can appear at activityRunning, a little before the
|
|
975
|
-
// worker emits ready and writes the terminal sidecar; a later poll should
|
|
976
|
-
// still auto-open the terminal block when it arrives.
|
|
977
|
-
return focusVisible && focusHasTerminal;
|
|
978
|
-
}
|
|
979
|
-
function buildCardDescriptors(snap) {
|
|
980
|
-
const byId = new Map(snap.activities.map((a) => [a.activityId, a]));
|
|
234
|
+
export function buildCardDescriptors(snap) {
|
|
235
|
+
const byId = new Map(snap.activities.map((activity) => [activity.activityId, activity]));
|
|
981
236
|
const used = new Set();
|
|
982
237
|
const out = [];
|
|
983
238
|
for (const node of snap.nodes) {
|
|
984
239
|
const activity = (node.activityId ? byId.get(node.activityId) : undefined) ??
|
|
985
|
-
snap.activities.find((
|
|
240
|
+
snap.activities.find((candidate) => candidate.ownerNodeId === node.nodeId);
|
|
986
241
|
if (!activity) {
|
|
987
242
|
out.push({ key: `node:${node.nodeId}`, node });
|
|
988
243
|
continue;
|
|
@@ -1006,260 +261,49 @@ function buildCardDescriptors(snap) {
|
|
|
1006
261
|
}
|
|
1007
262
|
return out;
|
|
1008
263
|
}
|
|
1009
|
-
function
|
|
1010
|
-
|
|
1011
|
-
article.className = 'wf-io-card';
|
|
1012
|
-
article.dataset.wfCardKey = key;
|
|
1013
|
-
const head = document.createElement('div');
|
|
1014
|
-
head.className = 'wf-io-card-head';
|
|
1015
|
-
const terminalSlot = document.createElement('div');
|
|
1016
|
-
terminalSlot.className = 'wf-io-terminal-slot';
|
|
1017
|
-
const grid = document.createElement('div');
|
|
1018
|
-
grid.className = 'wf-io-grid';
|
|
1019
|
-
article.appendChild(head);
|
|
1020
|
-
article.appendChild(terminalSlot);
|
|
1021
|
-
article.appendChild(grid);
|
|
1022
|
-
return { article, head, terminalSlot, grid, currentTerminalUrl: null };
|
|
264
|
+
export function latestAttempt(activity) {
|
|
265
|
+
return activity?.attempts[activity.attempts.length - 1];
|
|
1023
266
|
}
|
|
1024
|
-
function
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (attempt) {
|
|
1030
|
-
entry.article.dataset.wfAttemptCard = attempt.attemptId;
|
|
1031
|
-
}
|
|
1032
|
-
else {
|
|
1033
|
-
delete entry.article.dataset.wfAttemptCard;
|
|
1034
|
-
}
|
|
1035
|
-
// Head: header (title + status badges) + meta (attemptId) + approval controls.
|
|
1036
|
-
// These all re-render every poll; no live state survives in them.
|
|
1037
|
-
const controls = renderApprovalControls(attempt, approval);
|
|
1038
|
-
entry.head.innerHTML = `
|
|
1039
|
-
<header>
|
|
1040
|
-
<div>
|
|
1041
|
-
<strong><code>${escapeHtml(title)}</code></strong>
|
|
1042
|
-
<span class="muted">${desc.activity ? escapeHtml(desc.activity.activityId) : escapeHtml(t('workflow.detail.notDispatched'))}</span>
|
|
1043
|
-
</div>
|
|
1044
|
-
<div>${desc.node ? statusBadge(desc.node.status) : ''} ${desc.activity ? statusBadge(desc.activity.status) : ''}</div>
|
|
1045
|
-
</header>
|
|
1046
|
-
<div class="wf-io-meta">
|
|
1047
|
-
${attempt ? `${escapeHtml(t('workflow.detail.attempt'))} <code>${escapeHtml(attempt.attemptId)}</code>` : escapeHtml(t('workflow.detail.noAttempt'))}
|
|
1048
|
-
</div>
|
|
1049
|
-
${controls}
|
|
1050
|
-
`;
|
|
1051
|
-
// Terminal slot: only re-render when the URL changes (worker
|
|
1052
|
-
// restart / attempt-id flip / live → replay handoff / resume start
|
|
1053
|
-
// or end). The iframe DOM node stays live through every same-URL
|
|
1054
|
-
// poll so its WebSocket (live / resume) / streaming xterm parser
|
|
1055
|
-
// (replay) isn't torn down.
|
|
1056
|
-
const desiredSurface = computeTerminalUrl(attempt, desc.activity, desc.io?.terminal, resume);
|
|
1057
|
-
const desiredUrl = desiredSurface?.url ?? null;
|
|
1058
|
-
if (desiredUrl !== entry.currentTerminalUrl) {
|
|
1059
|
-
if (desiredSurface === null) {
|
|
1060
|
-
entry.terminalSlot.innerHTML = '';
|
|
1061
|
-
}
|
|
1062
|
-
else {
|
|
1063
|
-
entry.terminalSlot.innerHTML = renderTerminalBlockHtml(desc.key, attempt, desc.activity, desc.io?.terminal, desiredSurface, openBlocks, resume);
|
|
1064
|
-
}
|
|
1065
|
-
entry.currentTerminalUrl = desiredUrl;
|
|
1066
|
-
}
|
|
1067
|
-
else if (desiredSurface !== null && desc.io?.terminal) {
|
|
1068
|
-
// Same URL — refresh only the summary meta (status badge text)
|
|
1069
|
-
// and resume action button state (pending / error) inline. The
|
|
1070
|
-
// iframe itself is left alone to preserve its browsing context.
|
|
1071
|
-
const summary = entry.terminalSlot.querySelector('details.wf-terminal-block > summary');
|
|
1072
|
-
if (summary) {
|
|
1073
|
-
const label = terminalSurfaceLabel(desiredSurface.kind);
|
|
1074
|
-
summary.innerHTML = `${escapeHtml(label)} ${terminalMeta(attempt, desc.io.terminal)}`;
|
|
1075
|
-
}
|
|
1076
|
-
if (attempt) {
|
|
1077
|
-
refreshResumeActions(entry.terminalSlot, attempt, desc.activity, desc.io.terminal, desiredSurface, resume);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
// Grid: previews other than terminal. Wipe + rebuild — no live
|
|
1081
|
-
// resources here so flicker doesn't matter.
|
|
1082
|
-
const keyPrefix = attempt?.attemptId ?? desc.activity?.activityId ?? desc.node?.nodeId ?? 'unknown';
|
|
1083
|
-
entry.grid.innerHTML = `
|
|
1084
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.authoredInput'), desc.io?.input, openBlocks)}
|
|
1085
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.resolvedInput'), desc.io?.resolvedInput, openBlocks)}
|
|
1086
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.output'), desc.io?.output, openBlocks)}
|
|
1087
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.executionLog'), desc.io?.log, openBlocks)}
|
|
1088
|
-
${desc.io?.waitPrompt ? renderPreviewBlock(keyPrefix, t('workflow.detail.waitPrompt'), desc.io.waitPrompt, openBlocks) : ''}
|
|
1089
|
-
`;
|
|
267
|
+
export function isOpenHumanGateAttempt(attempt) {
|
|
268
|
+
return !!attempt &&
|
|
269
|
+
attempt.status === 'waiting' &&
|
|
270
|
+
attempt.wait?.waitKind === 'human-gate' &&
|
|
271
|
+
!attempt.wait.resolution;
|
|
1090
272
|
}
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (terminal.error)
|
|
273
|
+
export function computeTerminalSurface(options) {
|
|
274
|
+
const { activity, attempt, resumeSession, runId, terminal } = options;
|
|
275
|
+
if (!terminal || terminal.error)
|
|
1095
276
|
return null;
|
|
1096
277
|
if (isLiveTerminal(attempt, terminal)) {
|
|
1097
|
-
return { kind: 'live', url: terminalReadOnlyUrl(terminal) };
|
|
278
|
+
return { kind: 'live', url: terminalReadOnlyUrl(terminal, options.host) };
|
|
1098
279
|
}
|
|
1099
|
-
if (!attempt || !activity)
|
|
1100
|
-
return null;
|
|
1101
|
-
if (!isReplayableTerminal(attempt, terminal))
|
|
280
|
+
if (!attempt || !activity || !isReplayableTerminal(attempt, terminal))
|
|
1102
281
|
return null;
|
|
1103
|
-
const
|
|
1104
|
-
if (
|
|
1105
|
-
return
|
|
1106
|
-
// Active resume session for this attempt swaps the iframe to the
|
|
1107
|
-
// resume worker's write-token PTY URL — keep the download link
|
|
1108
|
-
// pointing at the original terminal.log so users can still grab the
|
|
1109
|
-
// pre-resume transcript.
|
|
1110
|
-
const active = resume?.sessions.get(attempt.attemptId);
|
|
1111
|
-
if (active) {
|
|
1112
|
-
return {
|
|
1113
|
-
kind: 'resume',
|
|
1114
|
-
url: active.url,
|
|
1115
|
-
resumeId: active.resumeId,
|
|
1116
|
-
downloadUrl: terminalLogDownloadUrl(runId, activity.activityId, attempt.attemptId),
|
|
1117
|
-
};
|
|
282
|
+
const downloadUrl = terminalLogDownloadUrl(runId, activity.activityId, attempt.attemptId);
|
|
283
|
+
if (resumeSession) {
|
|
284
|
+
return { kind: 'resume', url: resumeSession.url, resumeId: resumeSession.resumeId, downloadUrl };
|
|
1118
285
|
}
|
|
1119
286
|
return {
|
|
1120
287
|
kind: 'replay',
|
|
1121
288
|
url: terminalReplayPageUrl(runId, activity.activityId, attempt.attemptId, !!terminal.hasPtyLog),
|
|
1122
|
-
downloadUrl
|
|
289
|
+
downloadUrl,
|
|
1123
290
|
};
|
|
1124
291
|
}
|
|
1125
|
-
function
|
|
1126
|
-
if (!terminal)
|
|
1127
|
-
return '';
|
|
1128
|
-
const label = terminalSurfaceLabel(surface.kind);
|
|
1129
|
-
const key = ioBlockKey(keyPrefix, label);
|
|
1130
|
-
const meta = terminalMeta(attempt, terminal);
|
|
1131
|
-
const openInTabLabel = terminalOpenInTabLabel(surface.kind);
|
|
1132
|
-
const downloadAction = (surface.kind === 'replay' || surface.kind === 'resume')
|
|
1133
|
-
? `<a class="btn-link" href="${escapeHtml(surface.downloadUrl)}" download>${escapeHtml(t('workflow.detail.downloadFullLog'))}</a>`
|
|
1134
|
-
: '';
|
|
1135
|
-
const resumeAction = attempt
|
|
1136
|
-
? renderResumeButtonHtml(attempt, activity, terminal, surface, resume)
|
|
1137
|
-
: '';
|
|
1138
|
-
const resumeStatus = attempt ? renderResumeStatusHtml(attempt.attemptId, resume) : '';
|
|
1139
|
-
return `<details class="wf-io-block wf-terminal-block" data-io-key="${escapeHtml(key)}"${openBlocks.has(key) ? ' open' : ''}>
|
|
1140
|
-
<summary>${escapeHtml(label)} ${meta}</summary>
|
|
1141
|
-
<div class="wf-terminal-actions">
|
|
1142
|
-
<a class="btn-link" href="${escapeHtml(surface.url)}" target="_blank" rel="noreferrer">${escapeHtml(openInTabLabel)}</a>
|
|
1143
|
-
${downloadAction}
|
|
1144
|
-
${resumeAction}
|
|
1145
|
-
</div>
|
|
1146
|
-
${resumeStatus}
|
|
1147
|
-
<iframe class="wf-terminal-frame" src="${escapeHtml(surface.url)}" title="${escapeHtml(label)}" loading="lazy"></iframe>
|
|
1148
|
-
</details>`;
|
|
1149
|
-
}
|
|
1150
|
-
function terminalSurfaceLabel(kind) {
|
|
292
|
+
export function terminalSurfaceLabel(kind) {
|
|
1151
293
|
if (kind === 'live')
|
|
1152
294
|
return t('workflow.detail.liveTerminal');
|
|
1153
295
|
if (kind === 'resume')
|
|
1154
296
|
return t('workflow.detail.terminalResume');
|
|
1155
297
|
return t('workflow.detail.terminalReplay');
|
|
1156
298
|
}
|
|
1157
|
-
function terminalOpenInTabLabel(kind) {
|
|
299
|
+
export function terminalOpenInTabLabel(kind) {
|
|
1158
300
|
if (kind === 'live')
|
|
1159
301
|
return t('workflow.detail.openTerminalNewTab');
|
|
1160
302
|
if (kind === 'resume')
|
|
1161
303
|
return t('workflow.detail.openResumeNewTab');
|
|
1162
304
|
return t('workflow.detail.openReplayNewTab');
|
|
1163
305
|
}
|
|
1164
|
-
|
|
1165
|
-
* CLI capability split — mirrors `src/workflows/attempt-resume.ts`:
|
|
1166
|
-
* - REQUIRES native cliSessionId: adapter has no botmux-sessionId fallback,
|
|
1167
|
-
* so resume can't proceed unless `cli_session_id` IPC was captured.
|
|
1168
|
-
* - USES botmux sessionId: adapter resumes by the original attempt sessionId
|
|
1169
|
-
* (now passed through as `originalSessionId` in worker init by codex's
|
|
1170
|
-
* server-side fix). cliSessionId is consumed when present but optional.
|
|
1171
|
-
* - Anything else (opencode / gemini / ...) is `resume_unsupported_cli`
|
|
1172
|
-
* server-side.
|
|
1173
|
-
*/
|
|
1174
|
-
const RESUME_REQUIRES_CLI_SESSION_ID = new Set(['antigravity', 'codex-app', 'cursor', 'mira']);
|
|
1175
|
-
const RESUME_USES_SESSION_ID = new Set(['aiden', 'coco', 'claude-code', 'seed', 'relay', 'codex', 'mtr', 'hermes', 'pi', 'mir']);
|
|
1176
|
-
function isResumeCapableCli(cliId) {
|
|
1177
|
-
return !!cliId && (RESUME_USES_SESSION_ID.has(cliId) || RESUME_REQUIRES_CLI_SESSION_ID.has(cliId));
|
|
1178
|
-
}
|
|
1179
|
-
function cliRequiresNativeSessionId(cliId) {
|
|
1180
|
-
return !!cliId && RESUME_REQUIRES_CLI_SESSION_ID.has(cliId);
|
|
1181
|
-
}
|
|
1182
|
-
function renderResumeButtonHtml(attempt, activity, terminal, surface, resume) {
|
|
1183
|
-
if (!resume)
|
|
1184
|
-
return '';
|
|
1185
|
-
if (surface.kind === 'live')
|
|
1186
|
-
return '';
|
|
1187
|
-
if (!activity)
|
|
1188
|
-
return '';
|
|
1189
|
-
const active = surface.kind === 'resume';
|
|
1190
|
-
const pending = resume.pending.has(attempt.attemptId);
|
|
1191
|
-
const dataAttrs = `data-wf-resume-attempt="${escapeHtml(attempt.attemptId)}" ` +
|
|
1192
|
-
`data-wf-resume-activity="${escapeHtml(activity.activityId)}"`;
|
|
1193
|
-
// data-wf-resume-button is the stable marker used by refreshResumeActions
|
|
1194
|
-
// to locate any prior resume button in-place — including disabled variants
|
|
1195
|
-
// that don't carry attempt/activity ids — so re-renders replace instead of
|
|
1196
|
-
// appending and creating duplicates each poll.
|
|
1197
|
-
if (active) {
|
|
1198
|
-
return `<button type="button" class="btn-link" data-wf-resume-button="1" data-wf-resume-action="end" ${dataAttrs}${pending ? ' disabled' : ''}>${escapeHtml(pending ? t('workflow.detail.resumeEnding') : t('workflow.detail.endResumeSession'))}</button>`;
|
|
1199
|
-
}
|
|
1200
|
-
if (!isResumeCapableCli(terminal.cliId)) {
|
|
1201
|
-
return `<button type="button" class="btn-link" data-wf-resume-button="1" disabled title="${escapeHtml(t('workflow.detail.resumeUnsupportedCli', { cliId: terminal.cliId ?? '?' }))}">${escapeHtml(t('workflow.detail.resumeSession'))}</button>`;
|
|
1202
|
-
}
|
|
1203
|
-
// Only CLIs that have NO botmux-sessionId fallback (antigravity, cursor)
|
|
1204
|
-
// hard-require cliSessionId — aiden / coco / claude-code / codex / mtr / hermes resume
|
|
1205
|
-
// through the original attempt sessionId on the server side now.
|
|
1206
|
-
if (cliRequiresNativeSessionId(terminal.cliId) && !terminal.cliSessionId) {
|
|
1207
|
-
return `<button type="button" class="btn-link" data-wf-resume-button="1" disabled title="${escapeHtml(t('workflow.detail.resumeMissingCliSession'))}">${escapeHtml(t('workflow.detail.resumeSession'))}</button>`;
|
|
1208
|
-
}
|
|
1209
|
-
return `<button type="button" class="btn-link" data-wf-resume-button="1" data-wf-resume-action="start" ${dataAttrs}${pending ? ' disabled' : ''}>${escapeHtml(pending ? t('workflow.detail.resumeStarting') : t('workflow.detail.resumeSession'))}</button>`;
|
|
1210
|
-
}
|
|
1211
|
-
function renderResumeStatusHtml(attemptId, resume) {
|
|
1212
|
-
if (!resume)
|
|
1213
|
-
return '';
|
|
1214
|
-
const err = resume.errors.get(attemptId);
|
|
1215
|
-
if (err) {
|
|
1216
|
-
return `<div class="hint-warn wf-resume-status" data-wf-resume-status="${escapeHtml(attemptId)}">${escapeHtml(err)}</div>`;
|
|
1217
|
-
}
|
|
1218
|
-
return '';
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Legacy bulk renderer kept for the no-cache fallback path (tests).
|
|
1222
|
-
* The live dashboard always goes through the surgical update path.
|
|
1223
|
-
*/
|
|
1224
|
-
function renderIOCardHtml(desc, openBlocks, approval, resume, focusAttemptId) {
|
|
1225
|
-
const attempt = latestAttempt(desc.activity);
|
|
1226
|
-
const title = desc.node?.nodeId ?? desc.activity?.ownerNodeId ?? desc.activity?.activityId ?? 'unknown';
|
|
1227
|
-
const keyPrefix = attempt?.attemptId ?? desc.activity?.activityId ?? desc.node?.nodeId ?? 'unknown';
|
|
1228
|
-
const controls = renderApprovalControls(attempt, approval);
|
|
1229
|
-
const focusClass = attempt?.attemptId === focusAttemptId ? ' is-focused' : '';
|
|
1230
|
-
const attemptAttr = attempt ? ` data-wf-attempt-card="${escapeHtml(attempt.attemptId)}"` : '';
|
|
1231
|
-
const terminalSurface = computeTerminalUrl(attempt, desc.activity, desc.io?.terminal, resume);
|
|
1232
|
-
const terminalHtml = terminalSurface
|
|
1233
|
-
? renderTerminalBlockHtml(keyPrefix, attempt, desc.activity, desc.io?.terminal, terminalSurface, openBlocks, resume)
|
|
1234
|
-
: '';
|
|
1235
|
-
return `<article class="wf-io-card${focusClass}" data-wf-card-key="${escapeHtml(desc.key)}"${attemptAttr}>
|
|
1236
|
-
<div class="wf-io-card-head">
|
|
1237
|
-
<header>
|
|
1238
|
-
<div>
|
|
1239
|
-
<strong><code>${escapeHtml(title)}</code></strong>
|
|
1240
|
-
<span class="muted">${desc.activity ? escapeHtml(desc.activity.activityId) : escapeHtml(t('workflow.detail.notDispatched'))}</span>
|
|
1241
|
-
</div>
|
|
1242
|
-
<div>${desc.node ? statusBadge(desc.node.status) : ''} ${desc.activity ? statusBadge(desc.activity.status) : ''}</div>
|
|
1243
|
-
</header>
|
|
1244
|
-
<div class="wf-io-meta">
|
|
1245
|
-
${attempt ? `${escapeHtml(t('workflow.detail.attempt'))} <code>${escapeHtml(attempt.attemptId)}</code>` : escapeHtml(t('workflow.detail.noAttempt'))}
|
|
1246
|
-
</div>
|
|
1247
|
-
${controls}
|
|
1248
|
-
</div>
|
|
1249
|
-
<div class="wf-io-terminal-slot">${terminalHtml}</div>
|
|
1250
|
-
<div class="wf-io-grid">
|
|
1251
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.authoredInput'), desc.io?.input, openBlocks)}
|
|
1252
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.resolvedInput'), desc.io?.resolvedInput, openBlocks)}
|
|
1253
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.output'), desc.io?.output, openBlocks)}
|
|
1254
|
-
${renderPreviewBlock(keyPrefix, t('workflow.detail.executionLog'), desc.io?.log, openBlocks)}
|
|
1255
|
-
${desc.io?.waitPrompt ? renderPreviewBlock(keyPrefix, t('workflow.detail.waitPrompt'), desc.io.waitPrompt, openBlocks) : ''}
|
|
1256
|
-
</div>
|
|
1257
|
-
</article>`;
|
|
1258
|
-
}
|
|
1259
|
-
function latestAttempt(activity) {
|
|
1260
|
-
return activity?.attempts[activity.attempts.length - 1];
|
|
1261
|
-
}
|
|
1262
|
-
function terminalMeta(attempt, terminal) {
|
|
306
|
+
export function terminalMetaParts(attempt, terminal) {
|
|
1263
307
|
const bits = [];
|
|
1264
308
|
if (terminal.error)
|
|
1265
309
|
bits.push(t('workflow.detail.error'));
|
|
@@ -1269,241 +313,46 @@ function terminalMeta(attempt, terminal) {
|
|
|
1269
313
|
bits.push(attempt.status);
|
|
1270
314
|
if (terminal.webPort > 0)
|
|
1271
315
|
bits.push(`:${terminal.webPort}`);
|
|
1272
|
-
return
|
|
316
|
+
return bits;
|
|
317
|
+
}
|
|
318
|
+
export function isResumeCapableCli(cliId) {
|
|
319
|
+
return !!cliId && (RESUME_USES_SESSION_ID.has(cliId) || RESUME_REQUIRES_CLI_SESSION_ID.has(cliId));
|
|
1273
320
|
}
|
|
1274
|
-
function
|
|
321
|
+
export function cliRequiresNativeSessionId(cliId) {
|
|
322
|
+
return !!cliId && RESUME_REQUIRES_CLI_SESSION_ID.has(cliId);
|
|
323
|
+
}
|
|
324
|
+
export function isLiveTerminal(attempt, terminal) {
|
|
1275
325
|
return terminal.status === 'live' &&
|
|
1276
326
|
terminal.webPort > 0 &&
|
|
1277
327
|
(attempt?.status === 'pending' || attempt?.status === 'running' || attempt?.status === 'effectAttempting');
|
|
1278
328
|
}
|
|
1279
|
-
|
|
1280
|
-
* Replay-eligibility: attempt has reached a terminal state and a worker
|
|
1281
|
-
* actually started (sessionId / startedAt non-empty) — meaning a
|
|
1282
|
-
* `terminal.log` file likely exists on disk for the replay HTML to fetch.
|
|
1283
|
-
* The replay page itself handles a 404 gracefully, but gating here keeps
|
|
1284
|
-
* us from rendering an iframe that's guaranteed to show "no log".
|
|
1285
|
-
*/
|
|
1286
|
-
function isReplayableTerminal(attempt, terminal) {
|
|
329
|
+
export function isReplayableTerminal(attempt, terminal) {
|
|
1287
330
|
const isAttemptTerminal = attempt.status === 'succeeded' ||
|
|
1288
331
|
attempt.status === 'failed' ||
|
|
1289
332
|
attempt.status === 'cancelled' ||
|
|
1290
333
|
attempt.status === 'timedOut';
|
|
1291
334
|
if (!isAttemptTerminal)
|
|
1292
335
|
return false;
|
|
1293
|
-
|
|
1294
|
-
return hadWorker;
|
|
336
|
+
return !!(terminal.sessionId || terminal.startedAt);
|
|
1295
337
|
}
|
|
1296
|
-
function terminalReadOnlyUrl(terminal) {
|
|
1297
|
-
const
|
|
1298
|
-
return `http://${
|
|
338
|
+
export function terminalReadOnlyUrl(terminal, host) {
|
|
339
|
+
const resolvedHost = (host ?? (typeof window !== 'undefined' ? window.location.hostname : '')) || '127.0.0.1';
|
|
340
|
+
return `http://${resolvedHost}:${terminal.webPort}`;
|
|
1299
341
|
}
|
|
1300
|
-
function terminalReplayPageUrl(runId, activityId, attemptId, hasPtyLog) {
|
|
1301
|
-
const
|
|
342
|
+
export function terminalReplayPageUrl(runId, activityId, attemptId, hasPtyLog) {
|
|
343
|
+
const params = new URLSearchParams({ runId, activityId, attemptId });
|
|
1302
344
|
if (hasPtyLog)
|
|
1303
|
-
|
|
1304
|
-
return `/assets/terminal-replay.html?${
|
|
345
|
+
params.set('hasPtyLog', '1');
|
|
346
|
+
return `/assets/terminal-replay.html?${params.toString()}`;
|
|
1305
347
|
}
|
|
1306
|
-
function terminalLogDownloadUrl(runId, activityId, attemptId) {
|
|
348
|
+
export function terminalLogDownloadUrl(runId, activityId, attemptId) {
|
|
1307
349
|
return (`/api/workflows/runs/${encodeURIComponent(runId)}` +
|
|
1308
350
|
`/attempts/${encodeURIComponent(activityId)}` +
|
|
1309
351
|
`/${encodeURIComponent(attemptId)}/terminal-log/raw?download=1`);
|
|
1310
352
|
}
|
|
1311
|
-
function
|
|
1312
|
-
const m = window.location.hash.match(/^#\/legacy-workflow\/([^/?#]+)/);
|
|
1313
|
-
if (!m)
|
|
1314
|
-
return null;
|
|
1315
|
-
try {
|
|
1316
|
-
return decodeURIComponent(m[1]);
|
|
1317
|
-
}
|
|
1318
|
-
catch {
|
|
1319
|
-
return null;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
function renderApprovalControls(attempt, approval) {
|
|
1323
|
-
if (!isOpenHumanGateAttempt(attempt))
|
|
1324
|
-
return '';
|
|
1325
|
-
const attemptId = attempt.attemptId;
|
|
1326
|
-
const comment = approval.comments.get(attemptId) ?? '';
|
|
1327
|
-
const resolving = approval.resolving.has(attemptId);
|
|
1328
|
-
const status = approval.statuses.get(attemptId);
|
|
1329
|
-
const statusClass = status?.kind === 'error' ? 'hint-warn' : 'hint-ok';
|
|
1330
|
-
return `<div class="wf-approval-box" data-wf-approval="${escapeHtml(attemptId)}">
|
|
1331
|
-
<label>
|
|
1332
|
-
<span>${escapeHtml(t('workflow.detail.approvalComment'))}</span>
|
|
1333
|
-
<textarea class="wf-approval-comment" data-wf-approval-comment="${escapeHtml(attemptId)}" rows="2" placeholder="${escapeHtml(t('workflow.detail.optionalComment'))}"${resolving ? ' disabled' : ''}>${escapeHtml(comment)}</textarea>
|
|
1334
|
-
</label>
|
|
1335
|
-
<div class="wf-approval-actions">
|
|
1336
|
-
<button type="button" class="primary" data-wf-approval-action="approve" data-wf-attempt-id="${escapeHtml(attemptId)}"${resolving ? ' disabled' : ''}>${escapeHtml(t('workflow.detail.approve'))}</button>
|
|
1337
|
-
<button type="button" data-wf-approval-action="reject" data-wf-attempt-id="${escapeHtml(attemptId)}"${resolving ? ' disabled' : ''}>${escapeHtml(t('workflow.detail.reject'))}</button>
|
|
1338
|
-
${resolving ? `<span class="muted">${escapeHtml(t('workflow.detail.submitting'))}</span>` : ''}
|
|
1339
|
-
</div>
|
|
1340
|
-
${status ? `<div class="${statusClass} wf-approval-status">${escapeHtml(status.text)}</div>` : ''}
|
|
1341
|
-
</div>`;
|
|
1342
|
-
}
|
|
1343
|
-
function isOpenHumanGateAttempt(attempt) {
|
|
1344
|
-
return !!attempt &&
|
|
1345
|
-
attempt.status === 'waiting' &&
|
|
1346
|
-
attempt.wait?.waitKind === 'human-gate' &&
|
|
1347
|
-
!attempt.wait.resolution;
|
|
1348
|
-
}
|
|
1349
|
-
function syncApprovalComments(root, comments) {
|
|
1350
|
-
root.querySelectorAll('textarea[data-wf-approval-comment]').forEach((el) => {
|
|
1351
|
-
const key = el.dataset.wfApprovalComment;
|
|
1352
|
-
if (!key)
|
|
1353
|
-
return;
|
|
1354
|
-
comments.set(key, el.value);
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
function attachResumeControls(root, resume) {
|
|
1358
|
-
root.querySelectorAll('button[data-wf-resume-action][data-wf-resume-attempt][data-wf-resume-activity]').forEach((button) => {
|
|
1359
|
-
if (button.dataset.wfResumeBound === '1')
|
|
1360
|
-
return;
|
|
1361
|
-
button.dataset.wfResumeBound = '1';
|
|
1362
|
-
button.addEventListener('click', () => {
|
|
1363
|
-
const attemptId = button.dataset.wfResumeAttempt;
|
|
1364
|
-
const activityId = button.dataset.wfResumeActivity;
|
|
1365
|
-
const action = button.dataset.wfResumeAction;
|
|
1366
|
-
if (!attemptId || !activityId)
|
|
1367
|
-
return;
|
|
1368
|
-
if (action === 'start')
|
|
1369
|
-
void resume.onStart(attemptId, activityId);
|
|
1370
|
-
else if (action === 'end')
|
|
1371
|
-
void resume.onEnd(attemptId, activityId);
|
|
1372
|
-
});
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
function refreshResumeActions(slot, attempt, activity, terminal, surface, resume) {
|
|
1376
|
-
const actions = slot.querySelector('.wf-terminal-actions');
|
|
1377
|
-
if (!actions)
|
|
1378
|
-
return;
|
|
1379
|
-
// Re-render the resume button area in-place so the surrounding
|
|
1380
|
-
// anchors (open-in-tab + download) stay stable. We tag-select the
|
|
1381
|
-
// existing button (if any) and replace its outerHTML so the new
|
|
1382
|
-
// listener pickup goes through the same data-wf-resume-bound gate.
|
|
1383
|
-
const existingButton = actions.querySelector('button[data-wf-resume-button="1"]');
|
|
1384
|
-
const html = renderResumeButtonHtml(attempt, activity, terminal, surface, resume);
|
|
1385
|
-
if (existingButton) {
|
|
1386
|
-
existingButton.outerHTML = html;
|
|
1387
|
-
}
|
|
1388
|
-
else if (html) {
|
|
1389
|
-
actions.insertAdjacentHTML('beforeend', html);
|
|
1390
|
-
}
|
|
1391
|
-
// Update / clear inline error hint.
|
|
1392
|
-
const details = slot.querySelector('details.wf-terminal-block');
|
|
1393
|
-
if (details) {
|
|
1394
|
-
const existingStatus = details.querySelector('.wf-resume-status');
|
|
1395
|
-
const statusHtml = renderResumeStatusHtml(attempt.attemptId, resume);
|
|
1396
|
-
if (existingStatus) {
|
|
1397
|
-
existingStatus.outerHTML = statusHtml;
|
|
1398
|
-
}
|
|
1399
|
-
else if (statusHtml) {
|
|
1400
|
-
actions.insertAdjacentHTML('afterend', statusHtml);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
attachResumeControls(slot, resume);
|
|
1404
|
-
}
|
|
1405
|
-
function attachApprovalControls(root, approval) {
|
|
1406
|
-
root.querySelectorAll('textarea[data-wf-approval-comment]').forEach((el) => {
|
|
1407
|
-
const key = el.dataset.wfApprovalComment;
|
|
1408
|
-
if (!key)
|
|
1409
|
-
return;
|
|
1410
|
-
el.addEventListener('input', () => {
|
|
1411
|
-
approval.comments.set(key, el.value);
|
|
1412
|
-
});
|
|
1413
|
-
});
|
|
1414
|
-
root.querySelectorAll('button[data-wf-approval-action][data-wf-attempt-id]').forEach((button) => {
|
|
1415
|
-
button.addEventListener('click', () => {
|
|
1416
|
-
const attemptId = button.dataset.wfAttemptId;
|
|
1417
|
-
const action = button.dataset.wfApprovalAction;
|
|
1418
|
-
if (!attemptId || (action !== 'approve' && action !== 'reject'))
|
|
1419
|
-
return;
|
|
1420
|
-
void approval.onResolve(attemptId, action);
|
|
1421
|
-
});
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
function renderPreviewBlock(keyPrefix, label, preview, openBlocks) {
|
|
1425
|
-
const key = ioBlockKey(keyPrefix, label);
|
|
1426
|
-
return `<details class="wf-io-block" data-io-key="${escapeHtml(key)}"${openBlocks.has(key) ? ' open' : ''}>
|
|
1427
|
-
<summary>${escapeHtml(label)} ${previewMeta(preview)}</summary>
|
|
1428
|
-
${renderPreview(preview)}
|
|
1429
|
-
</details>`;
|
|
1430
|
-
}
|
|
1431
|
-
function ioBlockKey(keyPrefix, label) {
|
|
1432
|
-
return `${keyPrefix}:${label}`;
|
|
1433
|
-
}
|
|
1434
|
-
function scrollFocusedAttemptIntoView(root, focusAttemptId) {
|
|
1435
|
-
if (!focusAttemptId)
|
|
1436
|
-
return false;
|
|
1437
|
-
for (const card of root.querySelectorAll('[data-wf-attempt-card]')) {
|
|
1438
|
-
if (card.dataset.wfAttemptCard !== focusAttemptId)
|
|
1439
|
-
continue;
|
|
1440
|
-
card.scrollIntoView({ block: 'center' });
|
|
1441
|
-
return true;
|
|
1442
|
-
}
|
|
1443
|
-
return false;
|
|
1444
|
-
}
|
|
1445
|
-
function syncIOBlockState(root, openBlocks, scrollTops) {
|
|
1446
|
-
root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
|
|
1447
|
-
const key = el.dataset.ioKey;
|
|
1448
|
-
if (!key)
|
|
1449
|
-
return;
|
|
1450
|
-
if (el.open)
|
|
1451
|
-
openBlocks.add(key);
|
|
1452
|
-
else
|
|
1453
|
-
openBlocks.delete(key);
|
|
1454
|
-
const pre = el.querySelector('.wf-io-pre');
|
|
1455
|
-
if (pre)
|
|
1456
|
-
scrollTops.set(key, pre.scrollTop);
|
|
1457
|
-
});
|
|
1458
|
-
}
|
|
1459
|
-
function attachIOBlockToggleTracking(root, openBlocks) {
|
|
1460
|
-
root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
|
|
1461
|
-
if (el.dataset.ioToggleBound === '1')
|
|
1462
|
-
return;
|
|
1463
|
-
el.dataset.ioToggleBound = '1';
|
|
1464
|
-
el.addEventListener('toggle', () => {
|
|
1465
|
-
const key = el.dataset.ioKey;
|
|
1466
|
-
if (!key)
|
|
1467
|
-
return;
|
|
1468
|
-
if (el.open)
|
|
1469
|
-
openBlocks.add(key);
|
|
1470
|
-
else
|
|
1471
|
-
openBlocks.delete(key);
|
|
1472
|
-
});
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
function restoreIOBlockScroll(root, scrollTops) {
|
|
1476
|
-
root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
|
|
1477
|
-
const key = el.dataset.ioKey;
|
|
1478
|
-
if (!key)
|
|
1479
|
-
return;
|
|
1480
|
-
const top = scrollTops.get(key);
|
|
1481
|
-
if (top === undefined)
|
|
1482
|
-
return;
|
|
1483
|
-
const pre = el.querySelector('.wf-io-pre');
|
|
1484
|
-
if (pre)
|
|
1485
|
-
pre.scrollTop = top;
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
function attachIOBlockScrollTracking(root, scrollTops) {
|
|
1489
|
-
root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
|
|
1490
|
-
const key = el.dataset.ioKey;
|
|
1491
|
-
if (!key)
|
|
1492
|
-
return;
|
|
1493
|
-
const pre = el.querySelector('.wf-io-pre');
|
|
1494
|
-
if (!pre)
|
|
1495
|
-
return;
|
|
1496
|
-
if (pre.dataset.ioScrollBound === '1')
|
|
1497
|
-
return;
|
|
1498
|
-
pre.dataset.ioScrollBound = '1';
|
|
1499
|
-
pre.addEventListener('scroll', () => {
|
|
1500
|
-
scrollTops.set(key, pre.scrollTop);
|
|
1501
|
-
});
|
|
1502
|
-
});
|
|
1503
|
-
}
|
|
1504
|
-
function previewMeta(preview) {
|
|
353
|
+
export function previewMetaParts(preview) {
|
|
1505
354
|
if (!preview)
|
|
1506
|
-
return
|
|
355
|
+
return [t('workflow.detail.empty')];
|
|
1507
356
|
const bits = [];
|
|
1508
357
|
if (preview.outputBytes !== undefined)
|
|
1509
358
|
bits.push(`${preview.outputBytes}B`);
|
|
@@ -1513,73 +362,23 @@ function previewMeta(preview) {
|
|
|
1513
362
|
bits.push(t('workflow.detail.error'));
|
|
1514
363
|
if (preview.outputHash)
|
|
1515
364
|
bits.push(short(preview.outputHash));
|
|
1516
|
-
return bits
|
|
365
|
+
return bits;
|
|
1517
366
|
}
|
|
1518
|
-
function
|
|
367
|
+
export function previewBody(preview) {
|
|
1519
368
|
if (!preview)
|
|
1520
|
-
return
|
|
1521
|
-
|
|
369
|
+
return '';
|
|
370
|
+
return preview.value !== undefined
|
|
1522
371
|
? JSON.stringify(preview.value, null, 2)
|
|
1523
372
|
: preview.text ?? '';
|
|
1524
|
-
const error = preview.error ? `<div class="muted error">${escapeHtml(preview.error)}</div>` : '';
|
|
1525
|
-
if (!body)
|
|
1526
|
-
return `${error}<div class="muted wf-io-empty">${escapeHtml(t('workflow.detail.noPreview'))}</div>`;
|
|
1527
|
-
return `${error}<pre class="wf-io-pre">${escapeHtml(body)}</pre>`;
|
|
1528
|
-
}
|
|
1529
|
-
function renderAttemptDetail(at) {
|
|
1530
|
-
const parts = [];
|
|
1531
|
-
if (at.effectAttempted)
|
|
1532
|
-
parts.push(`${escapeHtml(t('workflow.detail.effect'))} ${escapeHtml(at.effectAttempted.provider)}`);
|
|
1533
|
-
if (at.wait) {
|
|
1534
|
-
const res = at.wait.resolution
|
|
1535
|
-
? `${at.wait.resolution.kind}${at.wait.resolution.resolution ? ':' + at.wait.resolution.resolution : ''}`
|
|
1536
|
-
: t('workflow.detail.open');
|
|
1537
|
-
parts.push(`${escapeHtml(t('workflow.detail.wait'))} ${escapeHtml(at.wait.waitKind)} ${escapeHtml(res)}`);
|
|
1538
|
-
if (at.wait.deadlineAt !== undefined) {
|
|
1539
|
-
parts.push(`${escapeHtml(t('workflow.detail.deadline'))} ${escapeHtml(formatClock(at.wait.deadlineAt))}`);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
if (at.error) {
|
|
1543
|
-
const tag = `${at.error.errorCode}${at.error.errorClass ? ` · ${at.error.errorClass}` : ''}`;
|
|
1544
|
-
parts.push(`<span class="muted error">${escapeHtml(tag)}</span>`);
|
|
1545
|
-
if (at.error.errorMessage) {
|
|
1546
|
-
parts.push(`<span class="error wf-error-msg">${escapeHtml(at.error.errorMessage)}</span>`);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
if (at.output)
|
|
1550
|
-
parts.push(`${escapeHtml(t('workflow.detail.output'))} ${escapeHtml(short(at.output.outputHash))}`);
|
|
1551
|
-
if (at.runningMs !== undefined)
|
|
1552
|
-
parts.push(`${at.runningMs}ms`);
|
|
1553
|
-
return parts.length > 0 ? parts.join('<br/>') : '<span class="muted">-</span>';
|
|
1554
|
-
}
|
|
1555
|
-
function renderEvents(tbody, events) {
|
|
1556
|
-
tbody.innerHTML =
|
|
1557
|
-
events.length > 0
|
|
1558
|
-
? events.map(renderEventRow).join('')
|
|
1559
|
-
: `<tr><td colspan="7" class="empty">${escapeHtml(t('workflow.detail.noEvents'))}</td></tr>`;
|
|
1560
|
-
}
|
|
1561
|
-
function renderEventRow(ev) {
|
|
1562
|
-
const ctx = extractEventContext(ev.payload);
|
|
1563
|
-
return `<tr>
|
|
1564
|
-
<td>${eventSeqFromId(ev.eventId)}</td>
|
|
1565
|
-
<td><code>${escapeHtml(ev.type)}</code></td>
|
|
1566
|
-
<td>${escapeHtml(ev.actor)}</td>
|
|
1567
|
-
<td>${ctx.nodeId ? `<code>${escapeHtml(ctx.nodeId)}</code>` : '-'}</td>
|
|
1568
|
-
<td>${ctx.activityId ? `<code>${escapeHtml(ctx.activityId)}</code>` : '-'}</td>
|
|
1569
|
-
<td>${ctx.errorCode ? `<span class="muted error">${escapeHtml(ctx.errorCode)}</span>` : '-'}</td>
|
|
1570
|
-
<td title="${escapeHtml(new Date(ev.timestamp).toISOString())}">${escapeHtml(formatClock(ev.timestamp))}</td>
|
|
1571
|
-
</tr>`;
|
|
1572
373
|
}
|
|
1573
|
-
|
|
1574
|
-
// pulling the Node/Zod projection module into the dashboard bundle.
|
|
1575
|
-
function eventSeqFromId(eventId) {
|
|
374
|
+
export function eventSeqFromId(eventId) {
|
|
1576
375
|
const dash = eventId.lastIndexOf('-');
|
|
1577
376
|
if (dash < 0)
|
|
1578
377
|
return 0;
|
|
1579
378
|
const n = Number(eventId.slice(dash + 1));
|
|
1580
379
|
return Number.isFinite(n) ? n : 0;
|
|
1581
380
|
}
|
|
1582
|
-
function extractEventContext(payload) {
|
|
381
|
+
export function extractEventContext(payload) {
|
|
1583
382
|
if (!payload || typeof payload !== 'object' || 'ref' in payload)
|
|
1584
383
|
return {};
|
|
1585
384
|
const p = payload;
|
|
@@ -1596,27 +395,42 @@ function extractEventContext(payload) {
|
|
|
1596
395
|
}
|
|
1597
396
|
return out;
|
|
1598
397
|
}
|
|
1599
|
-
function
|
|
1600
|
-
if (!ev.payload || typeof ev.payload !== 'object' || 'ref' in ev.payload)
|
|
1601
|
-
return null;
|
|
1602
|
-
return ev.payload;
|
|
1603
|
-
}
|
|
1604
|
-
function clamp(value, min, max) {
|
|
398
|
+
export function clamp(value, min, max) {
|
|
1605
399
|
return Math.min(max, Math.max(min, value));
|
|
1606
400
|
}
|
|
1607
|
-
function short(value) {
|
|
401
|
+
export function short(value) {
|
|
1608
402
|
if (!value)
|
|
1609
403
|
return '-';
|
|
1610
404
|
return value.length > 18 ? value.slice(0, 10) + '...' + value.slice(-6) : value;
|
|
1611
405
|
}
|
|
1612
|
-
function shortText(value, max) {
|
|
406
|
+
export function shortText(value, max) {
|
|
1613
407
|
return value.length > max ? value.slice(0, max - 1) + '…' : value;
|
|
1614
408
|
}
|
|
1615
|
-
function formatClock(ms) {
|
|
409
|
+
export function formatClock(ms) {
|
|
1616
410
|
return new Date(ms).toLocaleTimeString([], {
|
|
1617
411
|
hour: '2-digit',
|
|
1618
412
|
minute: '2-digit',
|
|
1619
413
|
second: '2-digit',
|
|
1620
414
|
});
|
|
1621
415
|
}
|
|
416
|
+
function payloadRecord(event) {
|
|
417
|
+
if (!event.payload || typeof event.payload !== 'object' || 'ref' in event.payload)
|
|
418
|
+
return null;
|
|
419
|
+
return event.payload;
|
|
420
|
+
}
|
|
421
|
+
function isTerminalActivityEvent(type) {
|
|
422
|
+
return type === 'activitySucceeded' ||
|
|
423
|
+
type === 'activityFailed' ||
|
|
424
|
+
type === 'activityTimedOut' ||
|
|
425
|
+
type === 'activityCanceled';
|
|
426
|
+
}
|
|
427
|
+
function terminalStatusForEvent(type) {
|
|
428
|
+
if (type === 'activitySucceeded')
|
|
429
|
+
return 'succeeded';
|
|
430
|
+
if (type === 'activityCanceled')
|
|
431
|
+
return 'cancelled';
|
|
432
|
+
if (type === 'activityTimedOut')
|
|
433
|
+
return 'timedOut';
|
|
434
|
+
return 'failed';
|
|
435
|
+
}
|
|
1622
436
|
//# sourceMappingURL=workflows.js.map
|