botmux 2.87.0 → 2.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.en.md +1 -0
  2. package/README.md +1 -0
  3. package/dist/adapters/backend/liveness-gate.d.ts +38 -0
  4. package/dist/adapters/backend/liveness-gate.d.ts.map +1 -0
  5. package/dist/adapters/backend/liveness-gate.js +53 -0
  6. package/dist/adapters/backend/liveness-gate.js.map +1 -0
  7. package/dist/adapters/backend/tmux-backend.d.ts +21 -0
  8. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  9. package/dist/adapters/backend/tmux-backend.js +102 -27
  10. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  11. package/dist/adapters/backend/tmux-pipe-backend.d.ts +20 -0
  12. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  13. package/dist/adapters/backend/tmux-pipe-backend.js +85 -19
  14. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  15. package/dist/adapters/backend/zellij-observe-backend.d.ts +13 -0
  16. package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -1
  17. package/dist/adapters/backend/zellij-observe-backend.js +51 -12
  18. package/dist/adapters/backend/zellij-observe-backend.js.map +1 -1
  19. package/dist/adapters/cli/traex.d.ts.map +1 -1
  20. package/dist/adapters/cli/traex.js +4 -0
  21. package/dist/adapters/cli/traex.js.map +1 -1
  22. package/dist/adapters/cli/types.d.ts +6 -0
  23. package/dist/adapters/cli/types.d.ts.map +1 -1
  24. package/dist/bot-registry.d.ts +13 -0
  25. package/dist/bot-registry.d.ts.map +1 -1
  26. package/dist/bot-registry.js +3 -0
  27. package/dist/bot-registry.js.map +1 -1
  28. package/dist/cli.js +6 -0
  29. package/dist/cli.js.map +1 -1
  30. package/dist/core/command-handler.js +1 -1
  31. package/dist/core/command-handler.js.map +1 -1
  32. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  33. package/dist/core/dashboard-ipc-server.js +165 -5
  34. package/dist/core/dashboard-ipc-server.js.map +1 -1
  35. package/dist/core/dashboard-rows.d.ts +3 -0
  36. package/dist/core/dashboard-rows.d.ts.map +1 -1
  37. package/dist/core/dashboard-rows.js +4 -1
  38. package/dist/core/dashboard-rows.js.map +1 -1
  39. package/dist/core/session-activity.d.ts +1 -0
  40. package/dist/core/session-activity.d.ts.map +1 -1
  41. package/dist/core/session-activity.js +7 -4
  42. package/dist/core/session-activity.js.map +1 -1
  43. package/dist/core/session-create.d.ts +62 -0
  44. package/dist/core/session-create.d.ts.map +1 -0
  45. package/dist/core/session-create.js +131 -0
  46. package/dist/core/session-create.js.map +1 -0
  47. package/dist/core/session-manager.d.ts +40 -0
  48. package/dist/core/session-manager.d.ts.map +1 -1
  49. package/dist/core/session-manager.js +236 -2
  50. package/dist/core/session-manager.js.map +1 -1
  51. package/dist/core/worker-pool.d.ts.map +1 -1
  52. package/dist/core/worker-pool.js +78 -4
  53. package/dist/core/worker-pool.js.map +1 -1
  54. package/dist/daemon.d.ts.map +1 -1
  55. package/dist/daemon.js +11 -5
  56. package/dist/daemon.js.map +1 -1
  57. package/dist/dashboard/bot-payload.d.ts +1 -0
  58. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  59. package/dist/dashboard/bot-payload.js +1 -0
  60. package/dist/dashboard/bot-payload.js.map +1 -1
  61. package/dist/dashboard/session-cleanup.d.ts +30 -0
  62. package/dist/dashboard/session-cleanup.d.ts.map +1 -0
  63. package/dist/dashboard/session-cleanup.js +47 -0
  64. package/dist/dashboard/session-cleanup.js.map +1 -0
  65. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  66. package/dist/dashboard/web/bot-defaults.js +20 -5
  67. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  68. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  69. package/dist/dashboard/web/i18n.js +184 -2
  70. package/dist/dashboard/web/i18n.js.map +1 -1
  71. package/dist/dashboard/web/insights.d.ts.map +1 -1
  72. package/dist/dashboard/web/insights.js +355 -30
  73. package/dist/dashboard/web/insights.js.map +1 -1
  74. package/dist/dashboard/web/sessions.d.ts +2 -0
  75. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  76. package/dist/dashboard/web/sessions.js +457 -30
  77. package/dist/dashboard/web/sessions.js.map +1 -1
  78. package/dist/dashboard-web/app.js +767 -640
  79. package/dist/dashboard-web/style.css +257 -7
  80. package/dist/dashboard.js +213 -2
  81. package/dist/dashboard.js.map +1 -1
  82. package/dist/i18n/en.d.ts.map +1 -1
  83. package/dist/i18n/en.js +21 -1
  84. package/dist/i18n/en.js.map +1 -1
  85. package/dist/i18n/zh.d.ts.map +1 -1
  86. package/dist/i18n/zh.js +21 -1
  87. package/dist/i18n/zh.js.map +1 -1
  88. package/dist/im/lark/card-builder.d.ts +4 -5
  89. package/dist/im/lark/card-builder.d.ts.map +1 -1
  90. package/dist/im/lark/card-builder.js +113 -11
  91. package/dist/im/lark/card-builder.js.map +1 -1
  92. package/dist/im/lark/card-handler.d.ts +3 -2
  93. package/dist/im/lark/card-handler.d.ts.map +1 -1
  94. package/dist/im/lark/card-handler.js +167 -11
  95. package/dist/im/lark/card-handler.js.map +1 -1
  96. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  97. package/dist/im/lark/event-dispatcher.js +19 -5
  98. package/dist/im/lark/event-dispatcher.js.map +1 -1
  99. package/dist/im/lark/grant-pending.d.ts.map +1 -1
  100. package/dist/im/lark/grant-pending.js +9 -2
  101. package/dist/im/lark/grant-pending.js.map +1 -1
  102. package/dist/index-daemon.js +5 -0
  103. package/dist/index-daemon.js.map +1 -1
  104. package/dist/platform/bind.d.ts +2 -0
  105. package/dist/platform/bind.d.ts.map +1 -0
  106. package/dist/platform/bind.js +91 -0
  107. package/dist/platform/bind.js.map +1 -0
  108. package/dist/platform/binding.d.ts +22 -0
  109. package/dist/platform/binding.d.ts.map +1 -0
  110. package/dist/platform/binding.js +33 -0
  111. package/dist/platform/binding.js.map +1 -0
  112. package/dist/platform/tunnel-client.d.ts +15 -0
  113. package/dist/platform/tunnel-client.d.ts.map +1 -0
  114. package/dist/platform/tunnel-client.js +193 -0
  115. package/dist/platform/tunnel-client.js.map +1 -0
  116. package/dist/services/bot-config-store.d.ts.map +1 -1
  117. package/dist/services/bot-config-store.js +1 -0
  118. package/dist/services/bot-config-store.js.map +1 -1
  119. package/dist/services/git-worktree.d.ts +10 -0
  120. package/dist/services/git-worktree.d.ts.map +1 -1
  121. package/dist/services/git-worktree.js +71 -26
  122. package/dist/services/git-worktree.js.map +1 -1
  123. package/dist/services/grant-prefs-store.d.ts +3 -0
  124. package/dist/services/grant-prefs-store.d.ts.map +1 -1
  125. package/dist/services/grant-prefs-store.js +17 -2
  126. package/dist/services/grant-prefs-store.js.map +1 -1
  127. package/dist/services/insight/claude-span-reader.d.ts.map +1 -1
  128. package/dist/services/insight/claude-span-reader.js +18 -2
  129. package/dist/services/insight/claude-span-reader.js.map +1 -1
  130. package/dist/services/insight/codex-span-reader.d.ts.map +1 -1
  131. package/dist/services/insight/codex-span-reader.js +17 -1
  132. package/dist/services/insight/codex-span-reader.js.map +1 -1
  133. package/dist/services/insight/report.d.ts.map +1 -1
  134. package/dist/services/insight/report.js +48 -6
  135. package/dist/services/insight/report.js.map +1 -1
  136. package/dist/services/insight/types.d.ts +9 -0
  137. package/dist/services/insight/types.d.ts.map +1 -1
  138. package/dist/services/insight/types.js.map +1 -1
  139. package/dist/services/worktree-slug-ai.d.ts.map +1 -1
  140. package/dist/services/worktree-slug-ai.js +3 -1
  141. package/dist/services/worktree-slug-ai.js.map +1 -1
  142. package/dist/types.d.ts +12 -0
  143. package/dist/types.d.ts.map +1 -1
  144. package/dist/utils/crash-log.d.ts +22 -0
  145. package/dist/utils/crash-log.d.ts.map +1 -0
  146. package/dist/utils/crash-log.js +37 -0
  147. package/dist/utils/crash-log.js.map +1 -0
  148. package/dist/utils/input-gate.d.ts +10 -0
  149. package/dist/utils/input-gate.d.ts.map +1 -1
  150. package/dist/utils/input-gate.js +7 -0
  151. package/dist/utils/input-gate.js.map +1 -1
  152. package/dist/utils/stdio-epipe-guard.d.ts +39 -0
  153. package/dist/utils/stdio-epipe-guard.d.ts.map +1 -0
  154. package/dist/utils/stdio-epipe-guard.js +67 -0
  155. package/dist/utils/stdio-epipe-guard.js.map +1 -0
  156. package/dist/worker.js +210 -26
  157. package/dist/worker.js.map +1 -1
  158. package/dist/workflows/daemon-spawn.d.ts +2 -0
  159. package/dist/workflows/daemon-spawn.d.ts.map +1 -1
  160. package/package.json +1 -1
@@ -2070,6 +2070,161 @@ fieldset legend {
2070
2070
 
2071
2071
  .bulk-bar[hidden] { display: none; }
2072
2072
  .bulk-bar #bulk-count { font-weight: 800; margin-right: 4px; }
2073
+ .idle-cleanup-bar {
2074
+ display: grid;
2075
+ grid-template-columns: minmax(0, 1fr) auto;
2076
+ align-items: center;
2077
+ gap: 8px 12px;
2078
+ margin: 10px 0 2px;
2079
+ padding: 8px;
2080
+ border-radius: var(--radius);
2081
+ border: 1px solid var(--border-soft);
2082
+ background: color-mix(in srgb, var(--surface) 92%, var(--bg));
2083
+ box-shadow: 0 10px 26px rgba(15, 23, 42, 0.06);
2084
+ }
2085
+ .idle-cleanup-summary,
2086
+ .idle-cleanup-controls {
2087
+ display: flex;
2088
+ align-items: center;
2089
+ min-width: 0;
2090
+ gap: 8px;
2091
+ }
2092
+ .idle-cleanup-controls {
2093
+ justify-content: flex-end;
2094
+ flex-wrap: wrap;
2095
+ }
2096
+ .idle-cleanup-dot {
2097
+ width: 8px;
2098
+ height: 8px;
2099
+ flex: 0 0 auto;
2100
+ border-radius: 50%;
2101
+ background: var(--danger);
2102
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 12%, transparent);
2103
+ }
2104
+ .idle-cleanup-bar.is-empty .idle-cleanup-dot {
2105
+ background: var(--muted);
2106
+ box-shadow: none;
2107
+ }
2108
+ .idle-cleanup-count,
2109
+ .idle-cleanup-status,
2110
+ .idle-cleanup-label {
2111
+ min-height: 32px;
2112
+ display: inline-flex;
2113
+ align-items: center;
2114
+ white-space: nowrap;
2115
+ }
2116
+ .idle-cleanup-count {
2117
+ padding: 0 12px;
2118
+ border-radius: 999px;
2119
+ background: var(--surface-muted);
2120
+ border: 1px solid var(--border-soft);
2121
+ color: var(--fg);
2122
+ font-size: 12.5px;
2123
+ font-weight: 800;
2124
+ font-variant-numeric: tabular-nums;
2125
+ }
2126
+ .idle-cleanup-label {
2127
+ color: var(--muted);
2128
+ font-size: 12.5px;
2129
+ font-weight: 700;
2130
+ }
2131
+ .idle-cleanup-thresholds {
2132
+ display: inline-grid;
2133
+ grid-auto-flow: column;
2134
+ gap: 2px;
2135
+ min-height: 32px;
2136
+ padding: 2px;
2137
+ border-radius: 999px;
2138
+ border: 1px solid var(--border-soft);
2139
+ background: var(--surface-muted);
2140
+ }
2141
+ .idle-cleanup-thresholds button {
2142
+ min-height: 26px;
2143
+ padding: 0 10px;
2144
+ border: 0;
2145
+ border-radius: 999px;
2146
+ background: transparent;
2147
+ box-shadow: none;
2148
+ color: var(--muted);
2149
+ font-size: 12px;
2150
+ font-weight: 800;
2151
+ font-variant-numeric: tabular-nums;
2152
+ }
2153
+ .idle-cleanup-thresholds button:hover:not(.active) {
2154
+ color: var(--fg);
2155
+ background: color-mix(in srgb, var(--border-soft) 60%, transparent);
2156
+ }
2157
+ .idle-cleanup-thresholds button.active {
2158
+ background: var(--surface);
2159
+ color: var(--fg);
2160
+ box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
2161
+ }
2162
+ .idle-cleanup-run {
2163
+ min-height: 32px;
2164
+ padding: 0 15px;
2165
+ font-weight: 800;
2166
+ transition: transform .16s ease, background-color .16s ease, border-color .16s ease, color .16s ease;
2167
+ }
2168
+ .idle-cleanup-run:hover:not(:disabled) {
2169
+ transform: translateY(-1px);
2170
+ }
2171
+ .idle-cleanup-run:active:not(:disabled) {
2172
+ transform: scale(.98);
2173
+ }
2174
+ .idle-cleanup-status {
2175
+ grid-column: 1 / -1;
2176
+ justify-self: end;
2177
+ padding: 0 11px;
2178
+ border-radius: 999px;
2179
+ border: 1px solid color-mix(in srgb, var(--success) 24%, var(--border-soft));
2180
+ background: color-mix(in srgb, var(--success) 8%, var(--surface));
2181
+ color: var(--success);
2182
+ font-size: 12px;
2183
+ font-weight: 800;
2184
+ }
2185
+ .idle-cleanup-status:empty {
2186
+ display: none;
2187
+ }
2188
+ :root[data-theme="dark"] .idle-cleanup-bar {
2189
+ background: rgba(255, 255, 255, 0.035);
2190
+ border-color: rgba(255, 255, 255, 0.08);
2191
+ box-shadow: none;
2192
+ }
2193
+ :root[data-theme="dark"] .idle-cleanup-count,
2194
+ :root[data-theme="dark"] .idle-cleanup-thresholds button.active {
2195
+ background: rgba(255, 255, 255, 0.06);
2196
+ }
2197
+ :root[data-theme="dark"] .idle-cleanup-thresholds {
2198
+ background: rgba(255, 255, 255, 0.035);
2199
+ border-color: rgba(255, 255, 255, 0.09);
2200
+ }
2201
+ :root[data-theme="dark"] .idle-cleanup-status {
2202
+ background: color-mix(in srgb, var(--success) 12%, transparent);
2203
+ }
2204
+ @media (max-width: 720px) {
2205
+ .idle-cleanup-bar {
2206
+ grid-template-columns: 1fr;
2207
+ align-items: stretch;
2208
+ }
2209
+ .idle-cleanup-controls {
2210
+ justify-content: flex-start;
2211
+ }
2212
+ .idle-cleanup-thresholds {
2213
+ flex: 1 1 180px;
2214
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2215
+ grid-auto-flow: column;
2216
+ }
2217
+ .idle-cleanup-thresholds button {
2218
+ min-width: 0;
2219
+ padding: 0 8px;
2220
+ }
2221
+ .idle-cleanup-run {
2222
+ flex: 1 1 118px;
2223
+ }
2224
+ .idle-cleanup-status {
2225
+ justify-self: start;
2226
+ }
2227
+ }
2073
2228
  input.row-select,
2074
2229
  #select-all { cursor: pointer; }
2075
2230
 
@@ -6850,12 +7005,66 @@ body.cp-fx-slice main { animation: cp-fx-slice 0.32s steps(2, end); }
6850
7005
  .insights-page .hbtrack { background: var(--ins-inner); border-radius: 6px; height: 14px; overflow: hidden; }
6851
7006
  .insights-page .hbfill { height: 100%; background: linear-gradient(90deg, var(--danger-soft), var(--bad)); border-radius: 6px; min-width: 3px; transform-origin: left; animation: insight-bar-grow .55s ease both; }
6852
7007
  .insights-page .hbval { font-size: 12px; color: var(--mut); text-align: right; white-space: nowrap; }
7008
+ .insights-page .hbval small { color: var(--ins-faint); font-size: 10px; margin-left: 5px; }
6853
7009
 
6854
- /* workbench: session list | detail (master-detail) */
6855
- .insight-workbench { display: grid; grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); gap: 14px; align-items: start; }
6856
- @media (max-width: 900px) { .insight-workbench { grid-template-columns: 1fr; } }
6857
- .insight-workbench > *, .insight-list-panel, .insight-detail { min-width: 0; }
6858
- .insight-list-panel, .insight-detail { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
7010
+ /* top-level tabs (总览 / 会话 / 分布 / 热点) — mirrors .detailtabbar */
7011
+ .insights-page .insight-tabs { display: flex; gap: 6px; border-bottom: 1px solid var(--line); margin: 4px 0 16px; }
7012
+ .insights-page .itab {
7013
+ background: transparent; color: var(--mut); border: 1px solid transparent; border-bottom: 0;
7014
+ border-radius: 8px 8px 0 0; padding: 8px 16px; font: inherit; font-size: 13px; cursor: pointer; margin-bottom: -1px;
7015
+ transition: color .12s ease, background .12s ease;
7016
+ }
7017
+ .insights-page .itab:hover { color: var(--fg); }
7018
+ .insights-page .itab.on { color: var(--fg); background: var(--panel); border-color: var(--line); font-weight: 600; }
7019
+ .insights-page .insight-panel[hidden] { display: none; }
7020
+
7021
+ /* filter-bar additions: project/time selects, analyzable toggle, clear */
7022
+ .insights-page .ins-select {
7023
+ background: var(--ins-inner); color: var(--fg); border: 1px solid var(--line); border-radius: 8px;
7024
+ padding: 6px 10px; font: inherit; font-size: 12px; max-width: 220px; cursor: pointer;
7025
+ }
7026
+ .insights-page .ins-toggle { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--mut); cursor: pointer; white-space: nowrap; }
7027
+ .insights-page .ins-toggle input { cursor: pointer; }
7028
+ .insights-page .ins-clear {
7029
+ background: transparent; color: var(--mut); border: 1px solid var(--line); border-radius: 8px;
7030
+ padding: 6px 12px; font: inherit; font-size: 12px; cursor: pointer;
7031
+ }
7032
+ .insights-page .ins-clear:hover { color: var(--fg); border-color: var(--acc); }
7033
+
7034
+ /* distribution histograms + hotspots grids */
7035
+ .insights-page .ins-hint { margin: 0 0 12px; font-size: 12px; }
7036
+ .insights-page .ihist-grid,
7037
+ .insights-page .hot-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; align-items: start; }
7038
+ .insights-page .ihist-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
7039
+ .insights-page .ihist-head h3 { margin: 0; }
7040
+ .insights-page .hot-sessions, .insights-page .hot-note { grid-column: 1 / -1; }
7041
+ .insights-page .hot-note { margin-top: 2px; }
7042
+
7043
+ /* Sessions tab: full-width list ⇆ full-width detail (mirrors ASI 会话, replaces
7044
+ the cramped master-detail side panel) */
7045
+ .insights-page #insight-list-view .insight-list-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
7046
+ .insights-page .sesssort { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
7047
+ .insights-page .sesssort-label { font-size: 11px; margin-right: 2px; }
7048
+ .insights-page .ins-back {
7049
+ background: transparent; color: var(--mut); border: 1px solid var(--line); border-radius: 8px;
7050
+ padding: 6px 14px; font: inherit; font-size: 12px; cursor: pointer; margin-bottom: 12px;
7051
+ }
7052
+ .insights-page .ins-back:hover { color: var(--fg); border-color: var(--acc); }
7053
+ .insights-page #insight-detail-view .insight-detail { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; }
7054
+ /* full-width rich rows */
7055
+ .insights-page .slist.wide { max-height: none; overflow: visible; gap: 10px; }
7056
+ .insights-page .slist.wide .srow { grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 18px; padding: 13px 16px; }
7057
+ .insights-page .slist.wide .srmain strong { -webkit-line-clamp: 1; font-size: 14px; }
7058
+ .insights-page .slist.wide .mph { max-width: 440px; height: 8px; }
7059
+ .insights-page .slist.wide .srstats { flex-wrap: nowrap; justify-content: flex-end; align-items: center; }
7060
+ .insights-page .slist.wide .srmsg { justify-self: end; }
7061
+ @media (max-width: 760px) {
7062
+ .insights-page .slist.wide .srow { grid-template-columns: 1fr; }
7063
+ .insights-page .slist.wide .srstats { justify-content: flex-start; flex-wrap: wrap; }
7064
+ }
7065
+
7066
+ /* session detail panel (full-width detail view) */
7067
+ .insight-detail { min-width: 0; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
6859
7068
  .insight-list-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
6860
7069
  .insight-list-head h3 { font-size: 13px; margin: 0; }
6861
7070
  .insight-list-head span { font-size: 11px; color: var(--mut); }
@@ -7274,6 +7483,10 @@ body.cp-fx-slice main { animation: cp-fx-slice 0.32s steps(2, end); }
7274
7483
  .insights-page .cbub-body .md-body { font-size: 13px; }
7275
7484
  .insights-page .cbub-foot { display: flex; justify-content: flex-end; }
7276
7485
  .insights-page .cbub-ops { background: var(--ins-recess); max-width: 88%; }
7486
+ /* agent narration ("说了什么") inside the agent bubble, above its operations */
7487
+ .insights-page .cbub-saywrap { display: flex; flex-direction: column; gap: 6px; margin: 2px 0 8px; }
7488
+ .insights-page .cbub-say { font-size: 13px; color: var(--fg); border-left: 2px solid var(--acc); padding-left: 9px; overflow-wrap: anywhere; word-break: break-word; }
7489
+ .insights-page .cbub-say .md-body { font-size: 13px; }
7277
7490
  .insights-page .cbub-ops-list { display: flex; flex-direction: column; gap: 3px; }
7278
7491
  .insights-page .cop-line { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 12px; padding: 3px 5px; border-radius: 6px; }
7279
7492
  .insights-page .cop-line.clickable { cursor: pointer; }
@@ -7360,12 +7573,49 @@ body.cp-fx-slice main { animation: cp-fx-slice 0.32s steps(2, end); }
7360
7573
  @keyframes insight-bar-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
7361
7574
  .insights-page .insights-metrics { animation: insight-fade-up .35s ease both; }
7362
7575
  .insights-page .insights-overview-grid { animation: insight-fade-up .35s ease both; animation-delay: .05s; }
7363
- .insights-page .insight-workbench { animation: insight-fade-up .35s ease both; animation-delay: .1s; }
7576
+ .insights-page .insight-panel { animation: insight-fade-up .35s ease both; animation-delay: .1s; }
7364
7577
  @media (prefers-reduced-motion: reduce) {
7365
7578
  .insights-page .insights-metrics,
7366
7579
  .insights-page .insights-overview-grid,
7367
- .insights-page .insight-workbench,
7580
+ .insights-page .insight-panel,
7368
7581
  .insights-page .hbfill { animation: none; }
7369
7582
  .insights-page .srow { transition: none; }
7370
7583
  .insights-page .srow:hover { transform: none; }
7371
7584
  }
7585
+
7586
+ /* ─── 创建会话 modal ─────────────────────────────────────────────────────── */
7587
+ .create-session-btn { white-space: nowrap; }
7588
+ .create-session-modal .cs-card { padding: 20px; }
7589
+ .create-session-modal textarea {
7590
+ width: 100%;
7591
+ box-sizing: border-box;
7592
+ resize: vertical;
7593
+ font: inherit;
7594
+ padding: 8px 10px;
7595
+ border: 1px solid var(--border);
7596
+ border-radius: 6px;
7597
+ background: var(--surface-muted);
7598
+ color: var(--fg);
7599
+ }
7600
+ .create-session-modal fieldset {
7601
+ border: 1px solid var(--border);
7602
+ border-radius: 6px;
7603
+ margin: 12px 0;
7604
+ padding: 10px 12px;
7605
+ }
7606
+ .create-session-modal legend { padding: 0 6px; color: var(--muted); font-size: 13px; }
7607
+ .create-session-modal .cs-bots {
7608
+ display: grid;
7609
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
7610
+ gap: 4px 12px;
7611
+ }
7612
+ .create-session-modal .cs-bot { display: flex; align-items: center; gap: 6px; cursor: pointer; }
7613
+ .create-session-modal .cs-mode label,
7614
+ .create-session-modal .cs-column label { display: inline-flex; align-items: center; gap: 6px; margin-right: 16px; cursor: pointer; }
7615
+ .create-session-modal fieldset small { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }
7616
+ .create-session-modal .cs-lead-row { margin: 12px 0; }
7617
+ .create-session-modal .cs-lead-row select { margin: 4px 0; padding: 6px 8px; }
7618
+ .create-session-modal .cs-advanced { margin: 12px 0; }
7619
+ .create-session-modal .cs-advanced summary { cursor: pointer; color: var(--muted); }
7620
+ .create-session-modal .cs-empty { color: var(--muted); margin: 0; }
7621
+ .create-session-modal .cs-warn { color: var(--warn, #c60); }
package/dist/dashboard.js CHANGED
@@ -48,6 +48,9 @@ import { installDashboardSkill, parseDashboardSkillInstallRequest, parseInstallL
48
48
  import { botDefaultsPayload, botSummaryPayload } from './dashboard/bot-payload.js';
49
49
  import { isValidRoleProfileId } from './services/role-profile-store.js';
50
50
  import { mergeSafeInsightOverviews } from './services/insight/report.js';
51
+ import { readPlatformBinding } from './platform/binding.js';
52
+ import { startPlatformTunnelClient } from './platform/tunnel-client.js';
53
+ import { cleanupIdleSessions, parseIdleCleanupHours } from './dashboard/session-cleanup.js';
51
54
  const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
52
55
  const TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
53
56
  /** Per-daemon budget for the cross-daemon insight overview fan-out — bounds
@@ -792,6 +795,54 @@ const server = createServer(async (req, res) => {
792
795
  });
793
796
  return jsonRes(res, 200, { sessions });
794
797
  }
798
+ if (req.method === 'POST' && url.pathname === '/api/sessions/cleanup-idle') {
799
+ let body;
800
+ try {
801
+ body = await readJsonBody(req);
802
+ }
803
+ catch {
804
+ return jsonRes(res, 400, { ok: false, error: 'bad_json' });
805
+ }
806
+ const olderThanHours = parseIdleCleanupHours(body?.olderThanHours);
807
+ if (!olderThanHours)
808
+ return jsonRes(res, 400, { ok: false, error: 'invalid_threshold' });
809
+ // WYSIWYG: the UI scopes cleanup to the rows currently visible under the
810
+ // page filters and sends their sessionIds, so the closed set matches the
811
+ // confirmed count. We still hand the scoped rows to cleanupIdleSessions,
812
+ // which re-validates each is a genuine idle candidate — a stale/forged id
813
+ // can never close a non-idle session. Omitting sessionIds (e.g. an older
814
+ // client) falls back to a deployment-wide sweep. Cap the id set so a giant
815
+ // body can't blow up the Set build.
816
+ const idScope = Array.isArray(body?.sessionIds)
817
+ ? new Set(body.sessionIds.slice(0, 10000).map(String))
818
+ : null;
819
+ const rows = aggregator.getSessions();
820
+ const scoped = idScope ? rows.filter(s => idScope.has(s.sessionId)) : rows;
821
+ const result = await cleanupIdleSessions(scoped, olderThanHours, async (s) => {
822
+ try {
823
+ const upstream = await proxyToDaemon(s.larkAppId, `/api/sessions/${encodeURIComponent(s.sessionId)}/close`, { method: 'POST' });
824
+ const text = await upstream.text();
825
+ let parsed = null;
826
+ try {
827
+ parsed = JSON.parse(text);
828
+ }
829
+ catch { /* tolerate */ }
830
+ // The daemon close route always replies 200 {ok:true}; treat anything
831
+ // else (incl. an unparseable/missing body) as a failure rather than a
832
+ // silent success.
833
+ const ok = upstream.ok && parsed?.ok === true;
834
+ return {
835
+ sessionId: s.sessionId,
836
+ ok,
837
+ error: ok ? undefined : (parsed?.error ?? `http_${upstream.status}`),
838
+ };
839
+ }
840
+ catch (e) {
841
+ return { sessionId: s.sessionId, ok: false, error: e?.message ?? String(e) };
842
+ }
843
+ });
844
+ return jsonRes(res, 200, result);
845
+ }
795
846
  if (req.method === 'GET' && url.pathname === '/api/insights/summary') {
796
847
  const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') ?? '200', 10) || 200, 1), 500);
797
848
  // Per-daemon timeout + isolate failures: an upstream insight parse can be
@@ -1249,7 +1300,7 @@ const server = createServer(async (req, res) => {
1249
1300
  return jsonRes(res, 200, { job });
1250
1301
  }
1251
1302
  let m;
1252
- if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(close|locate|resume)$/))) {
1303
+ if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(close|locate|resume|restart|start)$/))) {
1253
1304
  const sid = decodeURIComponent(m[1]);
1254
1305
  const op = m[2];
1255
1306
  const owner = aggregator.ownerOf(sid);
@@ -1969,7 +2020,8 @@ const server = createServer(async (req, res) => {
1969
2020
  return;
1970
2021
  }
1971
2022
  // PUT /api/bots/:appId/grant-prefs — proxy to that bot's daemon. Body carries
1972
- // any subset of `{ restrictGrantCommands?: boolean, messageQuotaDefaultLimit?: number|null }`.
2023
+ // any subset of `{ restrictGrantCommands?: boolean, autoGrantRequestCards?: boolean,
2024
+ // messageQuotaDefaultLimit?: number|null }`.
1973
2025
  let mBotGrantPrefs;
1974
2026
  if (req.method === 'PUT' && (mBotGrantPrefs = url.pathname.match(/^\/api\/bots\/([^/]+)\/grant-prefs$/))) {
1975
2027
  const appId = decodeURIComponent(mBotGrantPrefs[1]);
@@ -2097,6 +2149,133 @@ const server = createServer(async (req, res) => {
2097
2149
  res.end(upstreamJson ? JSON.stringify(upstreamJson) : upstreamText);
2098
2150
  return;
2099
2151
  }
2152
+ // Dashboard「创建会话」:建飞书群 + 拉选中的 bot,然后按协作模式给各 bot 拉起/暂存
2153
+ // 一条 chat-scope 会话。一起开工=每个被选 bot 各起一条;lead 分配=只起 lead,由它
2154
+ // 在群里 @ 拉起 sub bot。in_progress=立即开跑;backlog=入待办池(parked,等激活)。
2155
+ if (req.method === 'POST' && url.pathname === '/api/sessions/create') {
2156
+ let parsed;
2157
+ try {
2158
+ const chunks = [];
2159
+ for await (const c of req)
2160
+ chunks.push(c);
2161
+ parsed = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
2162
+ }
2163
+ catch {
2164
+ return jsonRes(res, 400, { ok: false, error: 'bad_json' });
2165
+ }
2166
+ const content = typeof parsed.content === 'string' ? parsed.content.replace(/\s+$/u, '') : '';
2167
+ if (!content.trim())
2168
+ return jsonRes(res, 400, { ok: false, error: 'empty_content' });
2169
+ if (content.length > 8000)
2170
+ return jsonRes(res, 400, { ok: false, error: 'content_too_long' });
2171
+ const selectedIds = Array.isArray(parsed.larkAppIds)
2172
+ ? Array.from(new Set(parsed.larkAppIds.filter((x) => typeof x === 'string')))
2173
+ : [];
2174
+ if (selectedIds.length === 0)
2175
+ return jsonRes(res, 400, { ok: false, error: 'larkAppIds_required' });
2176
+ const mode = parsed.mode === 'lead' ? 'lead' : parsed.mode === 'all' ? 'all' : null;
2177
+ if (!mode)
2178
+ return jsonRes(res, 400, { ok: false, error: 'bad_mode' });
2179
+ const column = parsed.column === 'backlog' ? 'backlog' : parsed.column === 'in_progress' ? 'in_progress' : null;
2180
+ if (!column)
2181
+ return jsonRes(res, 400, { ok: false, error: 'bad_column' });
2182
+ const bindWorkingDir = typeof parsed.bindWorkingDir === 'string' && parsed.bindWorkingDir.trim()
2183
+ ? parsed.bindWorkingDir.trim() : undefined;
2184
+ const name = typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim().slice(0, 60) : undefined;
2185
+ // 解析 creator:lead 模式 = lead bot;一起开工 = pickCreatorForGroup 在选中里挑一个在线的。
2186
+ let creatorLarkAppId;
2187
+ if (mode === 'lead') {
2188
+ const leadLarkAppId = typeof parsed.leadLarkAppId === 'string' ? parsed.leadLarkAppId : '';
2189
+ if (!leadLarkAppId || !selectedIds.includes(leadLarkAppId)) {
2190
+ return jsonRes(res, 400, { ok: false, error: 'bad_lead' });
2191
+ }
2192
+ if (!registry.getByAppId(leadLarkAppId))
2193
+ return jsonRes(res, 503, { ok: false, error: 'lead_offline' });
2194
+ creatorLarkAppId = leadLarkAppId;
2195
+ }
2196
+ else {
2197
+ const pick = pickCreatorForGroup(selectedIds, (id) => {
2198
+ const d = registry.getByAppId(id);
2199
+ return d ? { larkAppId: d.larkAppId, resolvedAllowedUsers: d.resolvedAllowedUsers ?? [] } : undefined;
2200
+ });
2201
+ if (!pick)
2202
+ return jsonRes(res, 503, { ok: false, error: 'no_online_daemon' });
2203
+ creatorLarkAppId = pick.creatorLarkAppId;
2204
+ }
2205
+ // creator 作用域里的操作者 open_id(首个 ou_ allowedUser)——用于邀请进群 + 转群主 + @通知。
2206
+ // 同时取 on_(union_id,租户内跨 app 稳定)做兜底邀请:lead 模式强制 creator=lead,
2207
+ // 万一 lead 的 allowlist 没有 ou_ 条目,open_id 解析不到、操作者就进不了群——union_id
2208
+ // 不受 app 作用域影响,仍能把人拉进来(createGroupWithBots 走 ownerUnionIds 通道)。
2209
+ const creatorDesc = registry.getByAppId(creatorLarkAppId);
2210
+ const allowed = creatorDesc.resolvedAllowedUsers ?? [];
2211
+ const userOpenId = allowed.find(u => u.startsWith('ou_'));
2212
+ const ownerUnionIds = allowed.filter(u => u.startsWith('on_'));
2213
+ // 建群(拉所有选中 bot + 邀请操作者 + 转群主 + @通知 + 可选绑 oncall 工作目录)。
2214
+ let groupResp = null;
2215
+ try {
2216
+ const groupUpstream = await proxyToDaemon(creatorLarkAppId, '/api/groups/create', {
2217
+ method: 'POST',
2218
+ headers: { 'content-type': 'application/json' },
2219
+ body: JSON.stringify({
2220
+ name,
2221
+ larkAppIds: selectedIds,
2222
+ userOpenIds: userOpenId ? [userOpenId] : [],
2223
+ ownerUnionIds,
2224
+ transferOwnerTo: userOpenId,
2225
+ notifyOwnerOpenId: userOpenId,
2226
+ bindWorkingDir,
2227
+ }),
2228
+ });
2229
+ groupResp = await groupUpstream.json().catch(() => null);
2230
+ if (!groupUpstream.ok || !groupResp?.ok || typeof groupResp.chatId !== 'string') {
2231
+ return jsonRes(res, 502, { ok: false, error: groupResp?.error ?? `group_create_http_${groupUpstream.status}` });
2232
+ }
2233
+ }
2234
+ catch {
2235
+ return jsonRes(res, 502, { ok: false, error: 'group_create_proxy_failed' });
2236
+ }
2237
+ const chatId = groupResp.chatId;
2238
+ const invalidBotIds = Array.isArray(groupResp.invalidBotIds) ? groupResp.invalidBotIds : [];
2239
+ // spawn 目标:lead 模式只有 lead;一起开工是所有成功入群的选中 bot。
2240
+ const joinedIds = selectedIds.filter(id => !invalidBotIds.includes(id) && !!registry.getByAppId(id));
2241
+ const targets = mode === 'lead'
2242
+ ? (joinedIds.includes(creatorLarkAppId) ? [creatorLarkAppId] : [])
2243
+ : joinedIds;
2244
+ if (targets.length === 0) {
2245
+ return jsonRes(res, 200, { ok: true, chatId, shareLink: groupResp.shareLink, spawned: [], failed: [], warning: 'no_spawn_target' });
2246
+ }
2247
+ const bots = liveBots();
2248
+ const nameOf = (id) => bots.find(b => b.larkAppId === id)?.botName ?? id;
2249
+ const spawned = [];
2250
+ const failed = [];
2251
+ await Promise.all(targets.map(async (appId) => {
2252
+ const role = mode === 'lead' ? 'lead' : (targets.length > 1 ? 'collab' : 'solo');
2253
+ // lead 的 coworker = 所有 sub(除自己);collab 的 coworker = 其它并列 bot(除自己)。
2254
+ const coworkerIds = (mode === 'lead' ? selectedIds : targets).filter(id => id !== appId);
2255
+ const coworkers = coworkerIds.map(id => ({ name: nameOf(id) }));
2256
+ try {
2257
+ const up = await proxyToDaemon(appId, '/api/sessions/spawn', {
2258
+ method: 'POST',
2259
+ headers: { 'content-type': 'application/json' },
2260
+ body: JSON.stringify({
2261
+ chatId, content, column, role, coworkers,
2262
+ postBanner: appId === creatorLarkAppId,
2263
+ }),
2264
+ });
2265
+ const b = await up.json().catch(() => null);
2266
+ if (up.ok && b?.ok)
2267
+ spawned.push(appId);
2268
+ else
2269
+ failed.push({ larkAppId: appId, error: b?.error ?? `http_${up.status}` });
2270
+ }
2271
+ catch (e) {
2272
+ failed.push({ larkAppId: appId, error: e?.message ?? String(e) });
2273
+ }
2274
+ }));
2275
+ return jsonRes(res, 200, {
2276
+ ok: true, chatId, shareLink: groupResp.shareLink, mode, column, spawned, failed,
2277
+ });
2278
+ }
2100
2279
  // Public SSE — relays aggregator's listener events
2101
2280
  if (req.method === 'GET' && url.pathname === '/events') {
2102
2281
  res.writeHead(200, {
@@ -2155,6 +2334,7 @@ listenWithProbe({
2155
2334
  logger.warn(`[dashboard] Failed to persist port to ${PORT_PATH}: ${e.message}`);
2156
2335
  }
2157
2336
  logger.info(`[dashboard] listening on ${config.dashboard.host}:${port}`);
2337
+ startPlatformTunnelIfBound();
2158
2338
  }).catch((err) => {
2159
2339
  logger.error(`[dashboard] could not bind near ${config.dashboard.host}:${config.dashboard.port} after probing — set BOTMUX_DASHBOARD_PORT to a free port. ${err.message}`);
2160
2340
  process.exit(1);
@@ -2169,12 +2349,43 @@ const federationSync = setInterval(() => {
2169
2349
  .catch(() => { });
2170
2350
  }, 2 * 60 * 1000);
2171
2351
  federationSync.unref();
2352
+ // 中心化平台隧道(已绑定才启动;每台机器一个,跑在 dashboard 进程里)
2353
+ let platformTunnel = null;
2354
+ function readBotmuxVersion() {
2355
+ try {
2356
+ const pkg = JSON.parse(readFileSync(join(dirname(__dirname), 'package.json'), 'utf8'));
2357
+ return pkg.version || 'unknown';
2358
+ }
2359
+ catch {
2360
+ return 'unknown';
2361
+ }
2362
+ }
2363
+ function startPlatformTunnelIfBound() {
2364
+ try {
2365
+ const binding = readPlatformBinding();
2366
+ if (!binding)
2367
+ return;
2368
+ const version = readBotmuxVersion();
2369
+ platformTunnel = startPlatformTunnelClient({
2370
+ binding,
2371
+ getDashboardPort: () => boundDashboardPort,
2372
+ getDashboardToken: () => activeToken,
2373
+ getVersion: () => version,
2374
+ log: (msg, extra) => logger.info(`[platform-tunnel] ${msg}${extra ? ' ' + JSON.stringify(extra) : ''}`),
2375
+ });
2376
+ logger.info(`[platform-tunnel] 绑定到 ${binding.platformUrl},启动隧道`);
2377
+ }
2378
+ catch (e) {
2379
+ logger.warn(`[platform-tunnel] 启动失败: ${e.message}`);
2380
+ }
2381
+ }
2172
2382
  // Graceful shutdown
2173
2383
  function shutdown() {
2174
2384
  for (const off of subs.values())
2175
2385
  off();
2176
2386
  subs.clear();
2177
2387
  registry.stop();
2388
+ platformTunnel?.stop();
2178
2389
  server.close(() => process.exit(0));
2179
2390
  // Hard-exit fallback after 5s
2180
2391
  setTimeout(() => process.exit(0), 5_000).unref();