botmux 2.114.0 → 3.0.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 (136) hide show
  1. package/dist/adapters/backend/riff-backend.d.ts +15 -0
  2. package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
  3. package/dist/adapters/backend/riff-backend.js +9 -3
  4. package/dist/adapters/backend/riff-backend.js.map +1 -1
  5. package/dist/core/dashboard-ipc-server.js +2 -2
  6. package/dist/core/dashboard-ipc-server.js.map +1 -1
  7. package/dist/dashboard/compat.d.ts +19 -0
  8. package/dist/dashboard/compat.d.ts.map +1 -0
  9. package/dist/dashboard/compat.js +38 -0
  10. package/dist/dashboard/compat.js.map +1 -0
  11. package/dist/dashboard/stale-chunk-module.d.ts +3 -0
  12. package/dist/dashboard/stale-chunk-module.d.ts.map +1 -0
  13. package/dist/dashboard/stale-chunk-module.js +55 -0
  14. package/dist/dashboard/stale-chunk-module.js.map +1 -0
  15. package/dist/dashboard/web/app.d.ts.map +1 -1
  16. package/dist/dashboard/web/app.js +64 -2
  17. package/dist/dashboard/web/app.js.map +1 -1
  18. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  19. package/dist/dashboard/web/bot-defaults-page.js +9 -7
  20. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  21. package/dist/dashboard/web/i18n.js +10 -10
  22. package/dist/dashboard/web/i18n.js.map +1 -1
  23. package/dist/dashboard/web/sessions-page.js +2 -2
  24. package/dist/dashboard/web/sessions-page.js.map +1 -1
  25. package/dist/dashboard/web/stale-chunk-reload.d.ts +16 -0
  26. package/dist/dashboard/web/stale-chunk-reload.d.ts.map +1 -0
  27. package/dist/dashboard/web/stale-chunk-reload.js +51 -0
  28. package/dist/dashboard/web/stale-chunk-reload.js.map +1 -0
  29. package/dist/dashboard-web/app.js +1 -1
  30. package/dist/dashboard-web/chunks/{bot-defaults-page-SRCBF7Z3.js → bot-defaults-page-MBORWWP7.js} +5 -5
  31. package/dist/dashboard-web/chunks/{chunk-3H6MVXXU.js → chunk-4QTBR7UF.js} +2 -2
  32. package/dist/dashboard-web/chunks/{chunk-77KD4TK2.js → chunk-7STGZ5BX.js} +1 -1
  33. package/dist/dashboard-web/chunks/{chunk-D2I5F4MY.js → chunk-EIO5AWDA.js} +1 -1
  34. package/dist/dashboard-web/chunks/{chunk-NGRUZCXP.js → chunk-G43RUPUN.js} +1 -1
  35. package/dist/dashboard-web/chunks/{connectors-page-RM3BJVMV.js → connectors-page-LKDT4WBG.js} +1 -1
  36. package/dist/dashboard-web/chunks/{groups-page-3D4YSYLA.js → groups-page-XKIWJ2TP.js} +1 -1
  37. package/dist/dashboard-web/chunks/{insights-page-CRFJKA54.js → insights-page-CBEZCYKV.js} +1 -1
  38. package/dist/dashboard-web/chunks/{monitor-room-HLK3XDT2.js → monitor-room-AQTHAWXA.js} +1 -1
  39. package/dist/dashboard-web/chunks/{monitoring-page-E3SNZFFN.js → monitoring-page-2R4NCNBQ.js} +1 -1
  40. package/dist/dashboard-web/chunks/{office-page-MKQ2TENK.js → office-page-5M6MQIIL.js} +1 -1
  41. package/dist/dashboard-web/chunks/{overview-page-ICFKKRLV.js → overview-page-N22HDZ2V.js} +1 -1
  42. package/dist/dashboard-web/chunks/{roles-page-2DFJ7YRU.js → roles-page-J6K2HWFX.js} +1 -1
  43. package/dist/dashboard-web/chunks/{schedules-page-S4WDIXH5.js → schedules-page-CGAQCB4J.js} +1 -1
  44. package/dist/dashboard-web/chunks/{sessions-page-4XWGUGB5.js → sessions-page-WJ2QYTIE.js} +2 -2
  45. package/dist/dashboard-web/chunks/{settings-page-OBTE4LYP.js → settings-page-GLRWV5KU.js} +1 -1
  46. package/dist/dashboard-web/chunks/{skills-page-CTRQJR47.js → skills-page-KQVW42QU.js} +1 -1
  47. package/dist/dashboard-web/chunks/{team-federation-page-GICLDKJP.js → team-federation-page-5PYUOV5M.js} +1 -1
  48. package/dist/dashboard-web/chunks/{v3-page-DI55IRL5.js → v3-page-DXPFILRZ.js} +1 -1
  49. package/dist/dashboard-web/chunks/{whiteboards-page-7KFWMILV.js → whiteboards-page-NUH6VZ4I.js} +1 -1
  50. package/dist/dashboard.js +26 -0
  51. package/dist/dashboard.js.map +1 -1
  52. package/dist/desktop/main/auto-start.d.ts +17 -0
  53. package/dist/desktop/main/auto-start.d.ts.map +1 -0
  54. package/dist/desktop/main/auto-start.js +37 -0
  55. package/dist/desktop/main/auto-start.js.map +1 -0
  56. package/dist/desktop/main/dashboard-compat.d.ts +25 -0
  57. package/dist/desktop/main/dashboard-compat.d.ts.map +1 -0
  58. package/dist/desktop/main/dashboard-compat.js +117 -0
  59. package/dist/desktop/main/dashboard-compat.js.map +1 -0
  60. package/dist/desktop/main/external-runtime.d.ts +29 -0
  61. package/dist/desktop/main/external-runtime.d.ts.map +1 -0
  62. package/dist/desktop/main/external-runtime.js +202 -0
  63. package/dist/desktop/main/external-runtime.js.map +1 -0
  64. package/dist/desktop/main/ipc.d.ts +20 -0
  65. package/dist/desktop/main/ipc.d.ts.map +1 -0
  66. package/dist/desktop/main/ipc.js +241 -0
  67. package/dist/desktop/main/ipc.js.map +1 -0
  68. package/dist/desktop/main/log-service.d.ts +4 -0
  69. package/dist/desktop/main/log-service.d.ts.map +1 -0
  70. package/dist/desktop/main/log-service.js +53 -0
  71. package/dist/desktop/main/log-service.js.map +1 -0
  72. package/dist/desktop/main/login-item.d.ts +10 -0
  73. package/dist/desktop/main/login-item.d.ts.map +1 -0
  74. package/dist/desktop/main/login-item.js +11 -0
  75. package/dist/desktop/main/login-item.js.map +1 -0
  76. package/dist/desktop/main/node-command.d.ts +22 -0
  77. package/dist/desktop/main/node-command.d.ts.map +1 -0
  78. package/dist/desktop/main/node-command.js +56 -0
  79. package/dist/desktop/main/node-command.js.map +1 -0
  80. package/dist/desktop/main/paths.d.ts +11 -0
  81. package/dist/desktop/main/paths.d.ts.map +1 -0
  82. package/dist/desktop/main/paths.js +13 -0
  83. package/dist/desktop/main/paths.js.map +1 -0
  84. package/dist/desktop/main/pm2-apps.d.ts +15 -0
  85. package/dist/desktop/main/pm2-apps.d.ts.map +1 -0
  86. package/dist/desktop/main/pm2-apps.js +97 -0
  87. package/dist/desktop/main/pm2-apps.js.map +1 -0
  88. package/dist/desktop/main/runtime-service.d.ts +49 -0
  89. package/dist/desktop/main/runtime-service.d.ts.map +1 -0
  90. package/dist/desktop/main/runtime-service.js +297 -0
  91. package/dist/desktop/main/runtime-service.js.map +1 -0
  92. package/dist/desktop/main/runtime-source.d.ts +17 -0
  93. package/dist/desktop/main/runtime-source.d.ts.map +1 -0
  94. package/dist/desktop/main/runtime-source.js +61 -0
  95. package/dist/desktop/main/runtime-source.js.map +1 -0
  96. package/dist/desktop/main/tray.d.ts +10 -0
  97. package/dist/desktop/main/tray.d.ts.map +1 -0
  98. package/dist/desktop/main/tray.js +27 -0
  99. package/dist/desktop/main/tray.js.map +1 -0
  100. package/dist/desktop/main/window.d.ts +16 -0
  101. package/dist/desktop/main/window.d.ts.map +1 -0
  102. package/dist/desktop/main/window.js +113 -0
  103. package/dist/desktop/main/window.js.map +1 -0
  104. package/dist/desktop/main.d.ts +2 -0
  105. package/dist/desktop/main.d.ts.map +1 -0
  106. package/dist/desktop/main.js +175 -0
  107. package/dist/desktop/main.js.map +1 -0
  108. package/dist/desktop/preload.d.ts +2 -0
  109. package/dist/desktop/preload.d.ts.map +1 -0
  110. package/dist/desktop/preload.js +24 -0
  111. package/dist/desktop/preload.js.map +1 -0
  112. package/dist/desktop/renderer/app.d.ts +35 -0
  113. package/dist/desktop/renderer/app.d.ts.map +1 -0
  114. package/dist/desktop/renderer/app.js +945 -0
  115. package/dist/desktop/renderer/app.js.map +1 -0
  116. package/dist/desktop/renderer/dashboard-url.d.ts +13 -0
  117. package/dist/desktop/renderer/dashboard-url.d.ts.map +1 -0
  118. package/dist/desktop/renderer/dashboard-url.js +98 -0
  119. package/dist/desktop/renderer/dashboard-url.js.map +1 -0
  120. package/dist/desktop/renderer/runtime-state.d.ts +8 -0
  121. package/dist/desktop/renderer/runtime-state.d.ts.map +1 -0
  122. package/dist/desktop/renderer/runtime-state.js +67 -0
  123. package/dist/desktop/renderer/runtime-state.js.map +1 -0
  124. package/dist/desktop/shared/types.d.ts +48 -0
  125. package/dist/desktop/shared/types.d.ts.map +1 -0
  126. package/dist/desktop/shared/types.js +2 -0
  127. package/dist/desktop/shared/types.js.map +1 -0
  128. package/dist/desktop/smoke.d.ts +28 -0
  129. package/dist/desktop/smoke.d.ts.map +1 -0
  130. package/dist/desktop/smoke.js +258 -0
  131. package/dist/desktop/smoke.js.map +1 -0
  132. package/dist/utils/version-info.d.ts +25 -0
  133. package/dist/utils/version-info.d.ts.map +1 -0
  134. package/dist/utils/version-info.js +48 -0
  135. package/dist/utils/version-info.js.map +1 -0
  136. package/package.json +9 -1
@@ -0,0 +1,945 @@
1
+ import { currentDashboardUrlFromSrc, dashboardRouteFromUrl, dashboardRouteMatches, normalizeDashboardRoute, routeDashboardUrl as buildDashboardUrl, } from './dashboard-url.js';
2
+ import { buildRuntimeMeta, buildVersionLine, emptyDashboardMessage, isRuntimeState, shouldKeepLoadedDashboardDuringDegradedState, } from './runtime-state.js';
3
+ const desktopLocaleStorageKey = 'botmux.dashboard.locale';
4
+ const logPollIntervalMs = 1500;
5
+ const navPointerDedupMs = 350;
6
+ const dashboardLocateRetryMs = 5000;
7
+ const desktopShellInjectedCss = `
8
+ .sidebar,
9
+ .topbar {
10
+ display: none !important;
11
+ }
12
+
13
+ .app-shell {
14
+ display: block !important;
15
+ grid-template-columns: minmax(0, 1fr) !important;
16
+ min-height: 100vh !important;
17
+ }
18
+
19
+ .chrome-body {
20
+ /* Hidden sidebar is removed from auto-placement, but explicit grid tracks remain. */
21
+ grid-template-columns: minmax(0, 1fr) !important;
22
+ gap: 0 !important;
23
+ }
24
+
25
+ .workspace {
26
+ grid-column: 1 / -1 !important;
27
+ min-width: 0 !important;
28
+ width: 100% !important;
29
+ }
30
+
31
+ .attention-strip {
32
+ margin-top: 0 !important;
33
+ }
34
+ `;
35
+ const messages = {
36
+ zh: {
37
+ 'app.subtitle': '飞书 AI CLI 控制台',
38
+ 'nav.overview': '总览',
39
+ 'nav.sessions': '会话',
40
+ 'nav.insights': '洞察',
41
+ 'nav.workflows': '工作流',
42
+ 'nav.groups': '群组',
43
+ 'nav.monitoring': '监控看板',
44
+ 'nav.schedules': '定时',
45
+ 'nav.whiteboards': '白板',
46
+ 'nav.roles': '角色管理',
47
+ 'nav.botDefaults': 'Bot 配置',
48
+ 'nav.skills': 'Skills',
49
+ 'nav.team': '团队',
50
+ 'nav.connectors': 'Webhook',
51
+ 'nav.office': '办公室',
52
+ 'nav.settings': '设置',
53
+ 'summary.onlineDaemons': '在线 Daemon',
54
+ 'summary.needsAttention': '需要处理',
55
+ 'summary.configuredBots': '已配置 Bot',
56
+ 'runtime.checking': '正在检查 CLI',
57
+ 'runtime.waitingBridge': '等待桌面桥接',
58
+ 'runtime.status.not_configured': '需要配置',
59
+ 'runtime.status.stopped': '已停止',
60
+ 'runtime.status.starting': '启动中',
61
+ 'runtime.status.running': '运行中',
62
+ 'runtime.status.degraded': '需要处理',
63
+ 'runtime.stateUnavailable': '状态不可用',
64
+ 'runtime.stateUnavailableDetail': '无法读取 CLI 状态:{error}',
65
+ 'runtime.bridgeUnavailable': '桌面桥接不可用',
66
+ 'runtime.bridgeUnavailableDetail': '重启 Botmux Desktop 以重新连接 IPC。',
67
+ 'runtimeSource.globalCli': '全局 CLI',
68
+ 'runtimeSource.none': '未连接 CLI',
69
+ 'version.unavailable': '版本信息不可用',
70
+ 'empty.notConfigured': '添加机器人后启动 CLI,即可加载控制台。',
71
+ 'empty.stopped': '启动 CLI 后加载控制台。',
72
+ 'empty.degraded': 'CLI 需要处理后才能加载控制台。',
73
+ 'empty.dashboardMissing': '控制台 URL 暂不可用。',
74
+ 'empty.stateUnavailable': 'CLI 状态不可用。',
75
+ 'empty.bridgeUnavailable': '桌面桥接不可用。',
76
+ 'empty.dashboardUnavailable': '控制台不可用:{error}',
77
+ 'empty.dashboardLocateFailed': '控制台不可用({reason}):{message}',
78
+ 'empty.startRuntime': '启动 CLI 后加载控制台。',
79
+ 'empty.startFirst': '请先启动 CLI,再打开控制台页面。',
80
+ 'runtime.restartMessage': '正在重启 CLI...',
81
+ 'action.start': '启动',
82
+ 'action.stop': '停止',
83
+ 'action.restart': '重启',
84
+ 'action.takeover': '连接当前 CLI',
85
+ 'action.createSession': '创建会话',
86
+ 'action.addBot': '添加机器人',
87
+ 'action.docs': '文档',
88
+ 'action.logs': '日志',
89
+ 'action.openHome': '打开目录',
90
+ 'action.copy': '复制',
91
+ 'action.openFolder': '打开文件夹',
92
+ 'action.close': '关闭',
93
+ 'action.requested': '已请求{label}...',
94
+ 'action.failed': '{label}失败:{error}',
95
+ 'path.home': 'Botmux 目录',
96
+ 'path.logs': '日志文件夹',
97
+ 'dashboard.openBotOnboarding': '正在打开添加机器人...',
98
+ 'dashboard.openCreateSession': '正在打开创建会话...',
99
+ 'dashboard.openingPage': '正在打开页面...',
100
+ 'dashboard.urlUnavailable': '控制台 URL 暂不可用。',
101
+ 'dashboard.openFailed': '打开页面失败:{error}',
102
+ 'login.launchAtLogin': '开机启动 App',
103
+ 'login.unavailable': '登录项不可用:{error}',
104
+ 'login.updateFailed': '登录项更新失败:{error}',
105
+ 'logs.title': '日志',
106
+ 'logs.initial': '打开日志后开始跟随输出。',
107
+ 'logs.noFiles': '暂未找到日志文件。',
108
+ 'logs.listFailed': '无法列出日志:{error}',
109
+ 'logs.recentPrefix': '[仅显示最近的日志输出]\n\n',
110
+ 'logs.empty': '暂无日志输出。',
111
+ 'logs.readFailed': '无法读取日志:{error}',
112
+ 'logs.copied': '日志已复制到剪贴板。',
113
+ 'logs.copyFailed': '复制失败:{error}',
114
+ 'path.openFailed': '{label}打开失败:{error}',
115
+ 'error.unknown': '未知错误',
116
+ },
117
+ en: {
118
+ 'app.subtitle': 'Feishu AI CLI Control',
119
+ 'nav.overview': 'Overview',
120
+ 'nav.sessions': 'Sessions',
121
+ 'nav.insights': 'Insight',
122
+ 'nav.workflows': 'Workflows',
123
+ 'nav.groups': 'Groups',
124
+ 'nav.monitoring': 'Monitoring',
125
+ 'nav.schedules': 'Schedules',
126
+ 'nav.whiteboards': 'Whiteboards',
127
+ 'nav.roles': 'Roles',
128
+ 'nav.botDefaults': 'Bot Profiles',
129
+ 'nav.skills': 'Skills',
130
+ 'nav.team': 'Team',
131
+ 'nav.connectors': 'Webhook',
132
+ 'nav.office': 'Office',
133
+ 'nav.settings': 'Settings',
134
+ 'summary.onlineDaemons': 'Online daemons',
135
+ 'summary.needsAttention': 'Needs attention',
136
+ 'summary.configuredBots': 'Configured bots',
137
+ 'runtime.checking': 'Checking CLI',
138
+ 'runtime.waitingBridge': 'Waiting for desktop bridge',
139
+ 'runtime.status.not_configured': 'Setup needed',
140
+ 'runtime.status.stopped': 'Stopped',
141
+ 'runtime.status.starting': 'Starting',
142
+ 'runtime.status.running': 'Running',
143
+ 'runtime.status.degraded': 'Needs attention',
144
+ 'runtime.stateUnavailable': 'State unavailable',
145
+ 'runtime.stateUnavailableDetail': 'Could not read CLI state: {error}',
146
+ 'runtime.bridgeUnavailable': 'Desktop bridge unavailable',
147
+ 'runtime.bridgeUnavailableDetail': 'Restart Botmux Desktop to reconnect IPC.',
148
+ 'runtimeSource.globalCli': 'Global CLI',
149
+ 'runtimeSource.none': 'No CLI connected',
150
+ 'version.unavailable': 'Version unavailable',
151
+ 'empty.notConfigured': 'Add a bot, then start the CLI to load the dashboard.',
152
+ 'empty.stopped': 'Start the CLI to load the dashboard.',
153
+ 'empty.degraded': 'CLI needs attention before the dashboard can load.',
154
+ 'empty.dashboardMissing': 'Dashboard URL is not available yet.',
155
+ 'empty.stateUnavailable': 'CLI state is unavailable.',
156
+ 'empty.bridgeUnavailable': 'Desktop bridge is unavailable.',
157
+ 'empty.dashboardUnavailable': 'Dashboard unavailable: {error}',
158
+ 'empty.dashboardLocateFailed': 'Dashboard unavailable ({reason}): {message}',
159
+ 'empty.startRuntime': 'Start the CLI to load the dashboard.',
160
+ 'empty.startFirst': 'Start the CLI before opening a dashboard page.',
161
+ 'runtime.restartMessage': 'Restarting CLI...',
162
+ 'action.start': 'Start',
163
+ 'action.stop': 'Stop',
164
+ 'action.restart': 'Restart',
165
+ 'action.takeover': 'Connect current CLI',
166
+ 'action.createSession': 'Create session',
167
+ 'action.addBot': 'Add bot',
168
+ 'action.docs': 'Docs',
169
+ 'action.logs': 'Logs',
170
+ 'action.openHome': 'Open folder',
171
+ 'action.copy': 'Copy',
172
+ 'action.openFolder': 'Open folder',
173
+ 'action.close': 'Close',
174
+ 'action.requested': '{label} requested...',
175
+ 'action.failed': '{label} failed: {error}',
176
+ 'path.home': 'Botmux home',
177
+ 'path.logs': 'Logs folder',
178
+ 'dashboard.openBotOnboarding': 'Opening add bot...',
179
+ 'dashboard.openCreateSession': 'Opening create session...',
180
+ 'dashboard.openingPage': 'Opening page...',
181
+ 'dashboard.urlUnavailable': 'Dashboard URL is not available yet.',
182
+ 'dashboard.openFailed': 'Open page failed: {error}',
183
+ 'login.launchAtLogin': 'Launch app at login',
184
+ 'login.unavailable': 'Login item unavailable: {error}',
185
+ 'login.updateFailed': 'Login item update failed: {error}',
186
+ 'logs.title': 'Logs',
187
+ 'logs.initial': 'Open logs to begin tailing.',
188
+ 'logs.noFiles': 'No log files found yet.',
189
+ 'logs.listFailed': 'Could not list logs: {error}',
190
+ 'logs.recentPrefix': '[Showing the most recent log output]\n\n',
191
+ 'logs.empty': 'No log output yet.',
192
+ 'logs.readFailed': 'Could not read logs: {error}',
193
+ 'logs.copied': 'Logs copied to clipboard.',
194
+ 'logs.copyFailed': 'Copy failed: {error}',
195
+ 'path.openFailed': '{label} failed: {error}',
196
+ 'error.unknown': 'Unknown error',
197
+ },
198
+ };
199
+ const runtimeDot = byId('runtime-dot');
200
+ const runtimeStatus = byId('runtime-status');
201
+ const runtimeMeta = byId('runtime-meta');
202
+ const botCount = byId('bot-count');
203
+ const onlineCount = byId('online-count');
204
+ const attentionCount = byId('attention-count');
205
+ const startBtn = byId('start-btn');
206
+ const stopBtn = byId('stop-btn');
207
+ const restartBtn = byId('restart-btn');
208
+ const takeoverBtn = byId('takeover-btn');
209
+ const createSessionBtn = byId('create-session-btn');
210
+ const addBotBtn = byId('add-bot-btn');
211
+ const logsBtn = byId('logs-btn');
212
+ const homeBtn = byId('home-btn');
213
+ const loginToggle = byId('login-toggle');
214
+ const versionLine = byId('version-line');
215
+ const dashboardFrame = byId('dashboard-frame');
216
+ const emptyDashboard = byId('empty-dashboard');
217
+ const logsDrawer = byId('logs-drawer');
218
+ const logTarget = byId('log-target');
219
+ const copyLogsBtn = byId('copy-logs');
220
+ const openLogsBtn = byId('open-logs');
221
+ const closeLogsBtn = byId('close-logs');
222
+ const logOutput = byId('log-output');
223
+ const dashboardNavLinks = Array.from(document.querySelectorAll('[data-dashboard-route]'));
224
+ const localeButtons = Array.from(document.querySelectorAll('[data-locale]'));
225
+ let currentLocale = readStoredLocale();
226
+ let lastState = null;
227
+ let runtimeNotice = null;
228
+ let actionPending = false;
229
+ // Sequence async dashboard loads so slower state polls cannot overwrite newer UI.
230
+ let dashboardLoadToken = 0;
231
+ // Only dashboard-clearing actions should cancel Add Bot navigation in flight.
232
+ let dashboardGeneration = 0;
233
+ let dashboardRoute = null;
234
+ let dashboardLocateRetryTimer = null;
235
+ let desktopShellCssInjectedUrl = null;
236
+ let logTimer = null;
237
+ let logPollInFlight = false;
238
+ // Each drawer open gets a token; stale log promises must not write after close.
239
+ let logsOpenToken = 0;
240
+ let lastPointerNavigation = null;
241
+ class DashboardLocateError extends Error {
242
+ }
243
+ wireControls();
244
+ paintChrome();
245
+ paintDashboardNav('#/');
246
+ void initialize();
247
+ function byId(id) {
248
+ const element = document.getElementById(id);
249
+ if (!element) {
250
+ throw new Error(`Missing renderer element: ${id}`);
251
+ }
252
+ return element;
253
+ }
254
+ function getApi() {
255
+ return window.botmuxDesktop ?? null;
256
+ }
257
+ function readStoredLocale() {
258
+ const stored = window.localStorage.getItem(desktopLocaleStorageKey);
259
+ if (stored === 'zh' || stored === 'en')
260
+ return stored;
261
+ const languages = navigator.languages?.length ? navigator.languages : [navigator.language].filter(Boolean);
262
+ return languages.some(language => language.toLowerCase().startsWith('en')) ? 'en' : 'zh';
263
+ }
264
+ function t(key, params) {
265
+ const template = messages[currentLocale][key] ?? messages.zh[key] ?? key;
266
+ if (!params)
267
+ return template;
268
+ return template.replace(/\{(\w+)\}/g, (_, name) => {
269
+ const value = params[name];
270
+ return value === undefined || value === null ? `{${name}}` : String(value);
271
+ });
272
+ }
273
+ function paintChrome() {
274
+ document.documentElement.lang = currentLocale === 'zh' ? 'zh-CN' : 'en';
275
+ for (const element of document.querySelectorAll('[data-i18n]')) {
276
+ if (element === logOutput)
277
+ continue;
278
+ element.textContent = t(element.dataset.i18n ?? '');
279
+ }
280
+ if (logOutput.textContent === messages.zh['logs.initial'] ||
281
+ logOutput.textContent === messages.en['logs.initial']) {
282
+ logOutput.textContent = t('logs.initial');
283
+ }
284
+ for (const button of localeButtons) {
285
+ button.classList.toggle('active', button.dataset.locale === currentLocale);
286
+ button.setAttribute('aria-pressed', String(button.dataset.locale === currentLocale));
287
+ }
288
+ }
289
+ async function initialize() {
290
+ const api = getApi();
291
+ if (!api) {
292
+ paintBridgeUnavailable();
293
+ return;
294
+ }
295
+ await initializeLoginToggle(api);
296
+ await refreshState();
297
+ api.onStateChanged(state => {
298
+ // Treat push events as untrusted IPC data and fall back to polling if the
299
+ // payload is not a complete runtime state.
300
+ if (isRuntimeState(state)) {
301
+ lastState = state;
302
+ paintState(state);
303
+ void syncDashboardForState(state);
304
+ return;
305
+ }
306
+ void refreshState();
307
+ });
308
+ }
309
+ function wireControls() {
310
+ for (const button of localeButtons) {
311
+ button.addEventListener('click', () => {
312
+ setLocale(button.dataset.locale);
313
+ });
314
+ }
315
+ for (const link of dashboardNavLinks)
316
+ wireDashboardNavLink(link);
317
+ dashboardFrame.addEventListener('did-navigate-in-page', event => {
318
+ const url = event.url;
319
+ if (!url)
320
+ return;
321
+ dashboardRoute = dashboardRouteFromUrl(url);
322
+ paintDashboardNav(dashboardRoute);
323
+ });
324
+ dashboardFrame.addEventListener('dom-ready', () => {
325
+ void injectDesktopShellCss();
326
+ });
327
+ dashboardFrame.addEventListener('did-start-loading', () => {
328
+ desktopShellCssInjectedUrl = null;
329
+ });
330
+ dashboardFrame.addEventListener('did-finish-load', () => {
331
+ void injectDesktopShellCss();
332
+ });
333
+ startBtn.addEventListener('click', () => {
334
+ void runRuntimeAction(t('action.start'), api => api.start());
335
+ });
336
+ stopBtn.addEventListener('click', () => {
337
+ void runRuntimeAction(t('action.stop'), api => api.stop());
338
+ });
339
+ restartBtn.addEventListener('click', () => {
340
+ clearDashboard(t('runtime.restartMessage'));
341
+ void runRuntimeAction(t('action.restart'), api => api.restart());
342
+ });
343
+ takeoverBtn.addEventListener('click', () => {
344
+ clearDashboard(t('runtime.restartMessage'));
345
+ void runRuntimeAction(t('action.takeover'), api => api.takeover());
346
+ });
347
+ createSessionBtn.addEventListener('click', () => {
348
+ void openCreateSession();
349
+ });
350
+ addBotBtn.addEventListener('click', () => {
351
+ void openBotOnboarding();
352
+ });
353
+ logsBtn.addEventListener('click', () => {
354
+ void openLogsDrawer();
355
+ });
356
+ homeBtn.addEventListener('click', () => {
357
+ void openPath(t('path.home'), api => api.openBotmuxHome());
358
+ });
359
+ logTarget.addEventListener('change', () => {
360
+ void tailSelectedLogs();
361
+ });
362
+ copyLogsBtn.addEventListener('click', () => {
363
+ void copyLogs();
364
+ });
365
+ openLogsBtn.addEventListener('click', () => {
366
+ void openPath(t('path.logs'), api => api.openLogsDir());
367
+ });
368
+ closeLogsBtn.addEventListener('click', closeLogsDrawer);
369
+ }
370
+ function wireDashboardNavLink(link) {
371
+ link.addEventListener('pointerup', event => {
372
+ if (!isPrimaryUnmodifiedPointer(event))
373
+ return;
374
+ event.preventDefault();
375
+ const route = link.dataset.dashboardRoute ?? '#/';
376
+ // Electron webview focus can swallow the follow-up click after the sidebar
377
+ // visually receives focus, so pointerup gives native navigation a fallback.
378
+ lastPointerNavigation = { route, time: performance.now() };
379
+ void openDashboardRoute(route);
380
+ });
381
+ link.addEventListener('click', event => {
382
+ event.preventDefault();
383
+ const route = link.dataset.dashboardRoute ?? '#/';
384
+ if (isRecentPointerNavigation(route))
385
+ return;
386
+ void openDashboardRoute(route);
387
+ });
388
+ }
389
+ function isPrimaryUnmodifiedPointer(event) {
390
+ return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
391
+ }
392
+ function isRecentPointerNavigation(route) {
393
+ if (!lastPointerNavigation || lastPointerNavigation.route !== route)
394
+ return false;
395
+ return performance.now() - lastPointerNavigation.time < navPointerDedupMs;
396
+ }
397
+ function setLocale(locale) {
398
+ if (locale !== 'zh' && locale !== 'en')
399
+ return;
400
+ if (locale === currentLocale)
401
+ return;
402
+ currentLocale = locale;
403
+ window.localStorage.setItem(desktopLocaleStorageKey, currentLocale);
404
+ runtimeNotice = null;
405
+ paintChrome();
406
+ if (lastState) {
407
+ paintState(lastState);
408
+ if (dashboardFrame.dataset.visible === 'false') {
409
+ clearDashboard(emptyDashboardMessage(lastState, t));
410
+ }
411
+ }
412
+ syncDashboardLocale();
413
+ }
414
+ function syncDashboardLocale() {
415
+ const src = dashboardFrame.getAttribute('src');
416
+ if (!src || src === 'about:blank')
417
+ return;
418
+ dashboardFrame.setAttribute('src', routeDashboardUrl(src));
419
+ }
420
+ async function refreshState() {
421
+ const api = getApi();
422
+ if (!api) {
423
+ paintBridgeUnavailable();
424
+ return;
425
+ }
426
+ try {
427
+ const state = await readRuntimeState(api);
428
+ lastState = state;
429
+ paintState(state);
430
+ await syncDashboardForState(state);
431
+ }
432
+ catch (error) {
433
+ runtimeDot.dataset.status = 'degraded';
434
+ runtimeStatus.textContent = t('runtime.stateUnavailable');
435
+ setRuntimeMetaText(t('runtime.stateUnavailableDetail', { error: formatError(error) }));
436
+ versionLine.textContent = t('version.unavailable');
437
+ paintControls(null);
438
+ paintRuntimeCounts(null);
439
+ clearDashboard(t('empty.stateUnavailable'));
440
+ }
441
+ }
442
+ async function readRuntimeState(api) {
443
+ const state = await api.getState();
444
+ if (isRuntimeState(state))
445
+ return state;
446
+ throw new Error('Invalid runtime state from desktop IPC');
447
+ }
448
+ function paintState(state) {
449
+ runtimeDot.dataset.status = state.status;
450
+ runtimeStatus.textContent = t(`runtime.status.${state.status}`);
451
+ setRuntimeMetaText(runtimeNotice ?? state.message ?? buildRuntimeMeta(state, t));
452
+ const versionText = buildVersionLine(state);
453
+ versionLine.textContent = versionText;
454
+ versionLine.title = state.runtimePath ? `${versionText}\n${state.runtimePath}` : versionText;
455
+ paintRuntimeCounts(state);
456
+ paintControls(state);
457
+ }
458
+ function setRuntimeMetaText(message) {
459
+ // The compact footer clamps long CLI messages, so keep the full text available on hover.
460
+ const trimmed = message.trim();
461
+ if (!trimmed) {
462
+ runtimeMeta.hidden = true;
463
+ runtimeMeta.textContent = '';
464
+ runtimeMeta.title = '';
465
+ return;
466
+ }
467
+ runtimeMeta.hidden = false;
468
+ runtimeMeta.textContent = trimmed;
469
+ runtimeMeta.title = trimmed;
470
+ }
471
+ function paintRuntimeCounts(state) {
472
+ botCount.textContent = String(state?.botCount ?? 0);
473
+ onlineCount.textContent = String(state?.onlineDaemonCount ?? 0);
474
+ attentionCount.textContent = String(state?.attentionCount ?? 0);
475
+ }
476
+ function paintControls(state) {
477
+ const bridgeUnavailable = !getApi();
478
+ const disabled = bridgeUnavailable || actionPending;
479
+ const status = state?.status;
480
+ const unmanagedRuntime = Boolean(state && !state.runtimeManaged);
481
+ startBtn.disabled = disabled || unmanagedRuntime || status === 'running' || status === 'starting';
482
+ stopBtn.disabled =
483
+ disabled || unmanagedRuntime || !status || status === 'not_configured' || status === 'stopped' || status === 'starting';
484
+ restartBtn.disabled = disabled || unmanagedRuntime || !status || status === 'not_configured' || status === 'starting';
485
+ takeoverBtn.hidden = true;
486
+ takeoverBtn.disabled = true;
487
+ createSessionBtn.disabled = bridgeUnavailable || unmanagedRuntime;
488
+ addBotBtn.disabled = bridgeUnavailable || unmanagedRuntime;
489
+ logsBtn.disabled = bridgeUnavailable;
490
+ homeBtn.disabled = bridgeUnavailable;
491
+ }
492
+ function paintBridgeUnavailable() {
493
+ runtimeDot.dataset.status = 'degraded';
494
+ runtimeStatus.textContent = t('runtime.bridgeUnavailable');
495
+ setRuntimeMetaText(t('runtime.bridgeUnavailableDetail'));
496
+ versionLine.textContent = t('version.unavailable');
497
+ loginToggle.disabled = true;
498
+ paintRuntimeCounts(null);
499
+ paintControls(null);
500
+ clearDashboard(t('empty.bridgeUnavailable'));
501
+ setLogOutput(t('empty.bridgeUnavailable'));
502
+ }
503
+ async function syncDashboardForState(state) {
504
+ const api = getApi();
505
+ const cachedDashboardUrl = currentDashboardUrl();
506
+ if (cachedDashboardUrl && state.status === 'running') {
507
+ // Runtime monitor refreshes should not re-run dashboard URL discovery once
508
+ // the webview is loaded; PM2/CLI probes are much slower than a hash update.
509
+ cancelDashboardLocateRetry();
510
+ showDashboard(routeDashboardUrl(cachedDashboardUrl));
511
+ return;
512
+ }
513
+ if (cachedDashboardUrl && shouldKeepLoadedDashboardDuringDegradedState(state)) {
514
+ return;
515
+ }
516
+ const shouldLoad = Boolean(state.dashboardUrl) || state.status === 'running';
517
+ if (!shouldLoad) {
518
+ clearDashboard(emptyDashboardMessage(state, t));
519
+ return;
520
+ }
521
+ const token = ++dashboardLoadToken;
522
+ let dashboardUrl = state.dashboardUrl;
523
+ if (!dashboardUrl) {
524
+ if (!api) {
525
+ paintBridgeUnavailable();
526
+ return;
527
+ }
528
+ try {
529
+ dashboardUrl = await locateDashboardUrl(api);
530
+ }
531
+ catch (error) {
532
+ if (token === dashboardLoadToken) {
533
+ clearDashboard(formatDashboardError(error));
534
+ scheduleDashboardLocateRetry(state);
535
+ }
536
+ return;
537
+ }
538
+ }
539
+ if (token !== dashboardLoadToken)
540
+ return;
541
+ if (dashboardUrl) {
542
+ cancelDashboardLocateRetry();
543
+ showDashboard(routeDashboardUrl(dashboardUrl));
544
+ }
545
+ else {
546
+ clearDashboard(emptyDashboardMessage(state, t));
547
+ }
548
+ }
549
+ function showDashboard(url) {
550
+ cancelDashboardLocateRetry();
551
+ emptyDashboard.hidden = true;
552
+ setDashboardVisible(true);
553
+ paintDashboardNav(dashboardRoute ?? '#/');
554
+ if (dashboardFrame.getAttribute('src') !== url) {
555
+ desktopShellCssInjectedUrl = null;
556
+ dashboardFrame.setAttribute('src', url);
557
+ }
558
+ }
559
+ async function injectDesktopShellCss() {
560
+ const url = currentDashboardUrl();
561
+ if (!url || desktopShellCssInjectedUrl === url)
562
+ return;
563
+ if (typeof dashboardFrame.insertCSS !== 'function')
564
+ return;
565
+ desktopShellCssInjectedUrl = url;
566
+ try {
567
+ // Keep dashboard source code browser-first: Desktop owns native chrome, so it
568
+ // masks the embedded dashboard chrome from the webview boundary.
569
+ await dashboardFrame.insertCSS(desktopShellInjectedCss);
570
+ }
571
+ catch {
572
+ if (desktopShellCssInjectedUrl === url)
573
+ desktopShellCssInjectedUrl = null;
574
+ // CSS injection is best-effort; compat checks still prevent unsafe embeds.
575
+ }
576
+ }
577
+ function clearDashboard(message) {
578
+ cancelDashboardLocateRetry();
579
+ dashboardRoute = null;
580
+ dashboardGeneration += 1;
581
+ dashboardLoadToken += 1;
582
+ desktopShellCssInjectedUrl = null;
583
+ paintDashboardNav('#/');
584
+ setDashboardVisible(false);
585
+ dashboardFrame.setAttribute('src', 'about:blank');
586
+ emptyDashboard.hidden = false;
587
+ emptyDashboard.textContent = message;
588
+ }
589
+ function scheduleDashboardLocateRetry(state) {
590
+ if (state.status !== 'running' || dashboardLocateRetryTimer !== null)
591
+ return;
592
+ // A compat endpoint can become available without changing the runtime state
593
+ // shape, so retry the empty incompatible view independently of state pushes.
594
+ dashboardLocateRetryTimer = window.setTimeout(() => {
595
+ dashboardLocateRetryTimer = null;
596
+ if (!lastState || lastState.status !== 'running' || currentDashboardUrl())
597
+ return;
598
+ void syncDashboardForState(lastState);
599
+ }, dashboardLocateRetryMs);
600
+ }
601
+ function cancelDashboardLocateRetry() {
602
+ if (dashboardLocateRetryTimer === null)
603
+ return;
604
+ window.clearTimeout(dashboardLocateRetryTimer);
605
+ dashboardLocateRetryTimer = null;
606
+ }
607
+ function setDashboardVisible(visible) {
608
+ // Keep the webview laid out even when hidden; Electron guests attached under
609
+ // display:none can keep the default 300x150 viewport after becoming visible.
610
+ dashboardFrame.dataset.visible = visible ? 'true' : 'false';
611
+ dashboardFrame.setAttribute('aria-hidden', visible ? 'false' : 'true');
612
+ }
613
+ async function runRuntimeAction(label, action) {
614
+ if (actionPending)
615
+ return;
616
+ const api = getApi();
617
+ if (!api) {
618
+ paintBridgeUnavailable();
619
+ return;
620
+ }
621
+ actionPending = true;
622
+ paintControls(lastState);
623
+ setRuntimeNotice(t('action.requested', { label }));
624
+ try {
625
+ const result = await action(api);
626
+ runtimeNotice = describeRunResult(label, result);
627
+ }
628
+ catch (error) {
629
+ runtimeNotice = t('action.failed', { label, error: formatError(error) });
630
+ }
631
+ finally {
632
+ // State refresh can take several seconds because it may spawn the CLI to
633
+ // query PM2/dashboard state; release controls first so the UI never looks
634
+ // permanently stuck after the runtime action itself has returned.
635
+ actionPending = false;
636
+ paintControls(lastState);
637
+ await refreshState();
638
+ }
639
+ }
640
+ function describeRunResult(label, result) {
641
+ if (!result || result.code === 0)
642
+ return null;
643
+ const detail = concise(result.stderr || result.stdout || `exit code ${result.code}`);
644
+ return t('action.failed', { label, error: detail });
645
+ }
646
+ async function openBotOnboarding() {
647
+ // The desktop shell hides the dashboard topbar, so pass a one-shot route
648
+ // action that lets the embedded dashboard open its own onboarding modal.
649
+ await openDashboardRoute('#/?open=bot-onboarding', t('dashboard.openBotOnboarding'));
650
+ }
651
+ async function openCreateSession() {
652
+ // The dashboard's create-session button lives in its hidden topbar; Desktop
653
+ // routes to Sessions so the page-owned React dialog is mounted before opening.
654
+ await openDashboardRoute('#/sessions?open=create-session', t('dashboard.openCreateSession'));
655
+ }
656
+ async function openDashboardRoute(route, notice = t('dashboard.openingPage')) {
657
+ const api = getApi();
658
+ if (!api) {
659
+ paintBridgeUnavailable();
660
+ return;
661
+ }
662
+ const generation = dashboardGeneration;
663
+ dashboardRoute = normalizeDashboardRoute(route);
664
+ paintDashboardNav(dashboardRoute);
665
+ setRuntimeNotice(notice);
666
+ const cachedDashboardUrl = currentDashboardUrl();
667
+ if (cachedDashboardUrl && lastState?.status === 'degraded' && shouldKeepLoadedDashboardDuringDegradedState(lastState)) {
668
+ // PM2/status degradation should not prevent in-webview hash navigation once
669
+ // a compatible dashboard has already loaded; protocol mismatch still clears below.
670
+ showDashboard(routeDashboardUrl(cachedDashboardUrl));
671
+ setRuntimeNotice(null);
672
+ return;
673
+ }
674
+ if (lastState?.status === 'degraded') {
675
+ setRuntimeNotice(null);
676
+ clearDashboard(emptyDashboardMessage(lastState, t));
677
+ return;
678
+ }
679
+ if (cachedDashboardUrl) {
680
+ // Sidebar route changes should stay inside the already-loaded webview; a
681
+ // fresh dashboard URL lookup can spawn the CLI and make navigation feel stuck.
682
+ showDashboard(routeDashboardUrl(cachedDashboardUrl));
683
+ setRuntimeNotice(null);
684
+ return;
685
+ }
686
+ try {
687
+ const dashboardUrl = await locateDashboardUrl(api);
688
+ if (generation !== dashboardGeneration)
689
+ return;
690
+ if (!dashboardUrl) {
691
+ setRuntimeNotice(t('dashboard.urlUnavailable'));
692
+ clearDashboard(t('empty.startFirst'));
693
+ return;
694
+ }
695
+ // Preserve the selected route across ordinary runtime state refreshes.
696
+ showDashboard(routeDashboardUrl(dashboardUrl));
697
+ setRuntimeNotice(null);
698
+ }
699
+ catch (error) {
700
+ if (generation === dashboardGeneration) {
701
+ setRuntimeNotice(error instanceof DashboardLocateError
702
+ ? error.message
703
+ : t('dashboard.openFailed', { error: formatError(error) }));
704
+ }
705
+ }
706
+ }
707
+ async function locateDashboardUrl(api) {
708
+ if (api.locateDashboard) {
709
+ const locate = await api.locateDashboard();
710
+ if (locate.ok)
711
+ return locate.url;
712
+ // Structured locate failures are user-facing state, not generic exceptions:
713
+ // preserve reason/message so the empty dashboard can say what is wrong.
714
+ throw new DashboardLocateError(formatDashboardLocateFailure(locate));
715
+ }
716
+ return api.getDashboardUrl();
717
+ }
718
+ function formatDashboardLocateFailure(locate) {
719
+ return t('empty.dashboardLocateFailed', {
720
+ reason: locate.reason,
721
+ message: locate.message || t('error.unknown'),
722
+ });
723
+ }
724
+ function formatDashboardError(error) {
725
+ if (error instanceof DashboardLocateError)
726
+ return error.message;
727
+ return t('empty.dashboardUnavailable', { error: formatError(error) });
728
+ }
729
+ function routeDashboardUrl(url) {
730
+ return buildDashboardUrl(url, dashboardRoute, {
731
+ locale: currentLocale,
732
+ runtimeVersion: lastState?.runtimeVersion ?? null,
733
+ });
734
+ }
735
+ function currentDashboardUrl() {
736
+ return currentDashboardUrlFromSrc(dashboardFrame.getAttribute('src'));
737
+ }
738
+ function paintDashboardNav(hash) {
739
+ for (const link of dashboardNavLinks) {
740
+ const route = normalizeDashboardRoute(link.dataset.dashboardRoute ?? '#/');
741
+ link.classList.toggle('active', dashboardRouteMatches(route, hash));
742
+ }
743
+ }
744
+ async function initializeLoginToggle(api) {
745
+ loginToggle.disabled = true;
746
+ try {
747
+ const state = await api.getLoginItem();
748
+ loginToggle.checked = Boolean(state.openAtLogin);
749
+ loginToggle.title = state.legacyAutostart ? `Legacy autostart detected at ${state.legacyPath ?? 'LaunchAgents'}` : '';
750
+ }
751
+ catch (error) {
752
+ setRuntimeNotice(t('login.unavailable', { error: formatError(error) }));
753
+ }
754
+ finally {
755
+ loginToggle.disabled = false;
756
+ }
757
+ loginToggle.addEventListener('change', () => {
758
+ void updateLoginItem(api, loginToggle.checked);
759
+ });
760
+ }
761
+ async function updateLoginItem(api, enabled) {
762
+ loginToggle.disabled = true;
763
+ try {
764
+ const state = await api.setLoginItem(enabled);
765
+ loginToggle.checked = Boolean(state.openAtLogin);
766
+ setRuntimeNotice(null);
767
+ }
768
+ catch (error) {
769
+ loginToggle.checked = !enabled;
770
+ setRuntimeNotice(t('login.updateFailed', { error: formatError(error) }));
771
+ }
772
+ finally {
773
+ loginToggle.disabled = false;
774
+ }
775
+ }
776
+ async function openLogsDrawer() {
777
+ const token = ++logsOpenToken;
778
+ logsDrawer.classList.add('open');
779
+ logsDrawer.setAttribute('aria-hidden', 'false');
780
+ await populateLogTargets(token);
781
+ if (!isLogsDrawerOpen(token))
782
+ return;
783
+ await pollLogs(token);
784
+ if (!isLogsDrawerOpen(token))
785
+ return;
786
+ startLogPolling(token);
787
+ }
788
+ function closeLogsDrawer() {
789
+ logsOpenToken += 1;
790
+ logsDrawer.classList.remove('open');
791
+ logsDrawer.setAttribute('aria-hidden', 'true');
792
+ stopLogPolling();
793
+ }
794
+ function startLogPolling(token) {
795
+ stopLogPolling();
796
+ logTimer = window.setInterval(() => {
797
+ void pollLogs(token);
798
+ }, logPollIntervalMs);
799
+ }
800
+ function stopLogPolling() {
801
+ if (logTimer !== null) {
802
+ window.clearInterval(logTimer);
803
+ logTimer = null;
804
+ }
805
+ }
806
+ async function pollLogs(token = logsOpenToken) {
807
+ if (logPollInFlight || !isLogsDrawerOpen(token))
808
+ return;
809
+ logPollInFlight = true;
810
+ try {
811
+ // Refresh targets on every poll so newly-created daemon logs appear without
812
+ // closing and reopening the drawer.
813
+ await populateLogTargets(token);
814
+ if (!isLogsDrawerOpen(token))
815
+ return;
816
+ if (!logTarget.disabled && logTarget.value) {
817
+ await tailSelectedLogs(token);
818
+ }
819
+ }
820
+ finally {
821
+ logPollInFlight = false;
822
+ }
823
+ }
824
+ function isLogsDrawerOpen(token = logsOpenToken) {
825
+ return token === logsOpenToken && logsDrawer.classList.contains('open');
826
+ }
827
+ async function populateLogTargets(token = logsOpenToken) {
828
+ const api = getApi();
829
+ if (!api) {
830
+ paintBridgeUnavailable();
831
+ return;
832
+ }
833
+ try {
834
+ const targets = await api.listLogTargets();
835
+ if (!isLogsDrawerOpen(token))
836
+ return;
837
+ const previous = logTarget.value;
838
+ if (!logTargetOptionsMatch(targets)) {
839
+ logTarget.replaceChildren();
840
+ for (const target of targets) {
841
+ const option = document.createElement('option');
842
+ option.value = target.id;
843
+ option.textContent = target.label;
844
+ logTarget.append(option);
845
+ }
846
+ }
847
+ const hasFiles = targets.some(target => target.files.length > 0);
848
+ logTarget.disabled = targets.length === 0 || !hasFiles;
849
+ if (targets.length > 0) {
850
+ const selected = targets.find(target => target.id === previous)?.id ?? targets[0].id;
851
+ logTarget.value = selected;
852
+ }
853
+ if (!hasFiles) {
854
+ setLogOutput(t('logs.noFiles'));
855
+ }
856
+ }
857
+ catch (error) {
858
+ if (!isLogsDrawerOpen(token))
859
+ return;
860
+ logTarget.disabled = true;
861
+ setLogOutput(t('logs.listFailed', { error: formatError(error) }));
862
+ }
863
+ }
864
+ async function tailSelectedLogs(token = logsOpenToken) {
865
+ if (!isLogsDrawerOpen(token))
866
+ return;
867
+ const api = getApi();
868
+ if (!api) {
869
+ paintBridgeUnavailable();
870
+ return;
871
+ }
872
+ const targetId = logTarget.value;
873
+ if (!targetId)
874
+ return;
875
+ try {
876
+ const tail = await api.tailLogs(targetId);
877
+ if (!isLogsDrawerOpen(token))
878
+ return;
879
+ const text = tail.text.trimEnd();
880
+ const prefix = tail.truncated ? t('logs.recentPrefix') : '';
881
+ const shouldStickToBottom = shouldAutoScrollLogs();
882
+ setLogOutput(text ? `${prefix}${text}` : t('logs.empty'));
883
+ if (shouldStickToBottom)
884
+ logOutput.scrollTop = logOutput.scrollHeight;
885
+ }
886
+ catch (error) {
887
+ if (!isLogsDrawerOpen(token))
888
+ return;
889
+ setLogOutput(t('logs.readFailed', { error: formatError(error) }));
890
+ }
891
+ }
892
+ function logTargetOptionsMatch(targets) {
893
+ const options = Array.from(logTarget.options);
894
+ return options.length === targets.length && options.every((option, index) => (option.value === targets[index]?.id && option.textContent === targets[index]?.label));
895
+ }
896
+ function shouldAutoScrollLogs() {
897
+ return logOutput.scrollHeight - logOutput.scrollTop - logOutput.clientHeight <= 8;
898
+ }
899
+ async function copyLogs() {
900
+ try {
901
+ await navigator.clipboard.writeText(logOutput.textContent ?? '');
902
+ setRuntimeNotice(t('logs.copied'));
903
+ }
904
+ catch (error) {
905
+ setRuntimeNotice(t('logs.copyFailed', { error: formatError(error) }));
906
+ }
907
+ }
908
+ async function openPath(label, opener) {
909
+ const api = getApi();
910
+ if (!api) {
911
+ paintBridgeUnavailable();
912
+ return;
913
+ }
914
+ try {
915
+ const result = await opener(api);
916
+ setRuntimeNotice(result ? t('path.openFailed', { label, error: concise(result) }) : null);
917
+ }
918
+ catch (error) {
919
+ setRuntimeNotice(t('path.openFailed', { label, error: formatError(error) }));
920
+ }
921
+ }
922
+ function setRuntimeNotice(message) {
923
+ runtimeNotice = message ? concise(message) : null;
924
+ if (lastState) {
925
+ paintState(lastState);
926
+ }
927
+ else if (runtimeNotice) {
928
+ setRuntimeMetaText(runtimeNotice);
929
+ }
930
+ }
931
+ function setLogOutput(message) {
932
+ logOutput.textContent = message;
933
+ }
934
+ function formatError(error) {
935
+ if (error instanceof Error)
936
+ return concise(error.message);
937
+ return concise(String(error));
938
+ }
939
+ function concise(value) {
940
+ const text = value.replace(/\s+/g, ' ').trim();
941
+ if (text.length <= 180)
942
+ return text || t('error.unknown');
943
+ return `${text.slice(0, 177)}...`;
944
+ }
945
+ //# sourceMappingURL=app.js.map