termdock 1.4.108 → 1.4.109

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 (76) hide show
  1. package/.desktop-dist/main.js +297 -11
  2. package/dist/client/assets/{ModelPreview-By3cPzJ9.js → ModelPreview-Cxk4IZLv.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-Dm9g0Pi3.js → abnfDiagram-VRR7QNED-Bz2HWRRX.js} +1 -1
  4. package/dist/client/assets/{arc-XntYYR9t.js → arc-D3vf35f_.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CW0kZlVv.js → architectureDiagram-ZJ3FMSHR-LI-OXsDp.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-DgMdh19W.js → blockDiagram-677ZJIJ3-Dm1JtLIo.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-DJC4m2Y6.js → c4Diagram-LMCZKHZV-DSkxlBnU.js} +1 -1
  8. package/dist/client/assets/channel-qZtjoGxW.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-CgE5oTjY.js → chunk-2Q5K7J3B-Wd2AO6yQ.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-B50lSI4-.js → chunk-32BRIVSS-CIBkFIAg.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-C984gGSO.js → chunk-5VM5RSS4-9veDV0NJ.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-C1jPbFiq.js → chunk-EX3LRPZG-DVi5mqrV.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-D4U9D5Kv.js → chunk-JWPE2WC7-DZc_3_5i.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-C4rLPnf_.js → chunk-MOJQB5TN-Czq1YGEA.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-CnSX68rt.js → chunk-RYQCIY6F-BN3UqH_7.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-LTTPDSzs.js → chunk-V7JOEXUC-BgDCEp_z.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-dCZ9yGOH.js → chunk-VR4S4FIN-BnyRVZSn.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-DiUZr3na.js → chunk-XXDRQBXY-KOa0rQIz.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-CnWXW1tk.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CnWXW1tk.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-Dw73llr7.js → cose-bilkent-JH36ORCC-B-P2BstB.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-CsB9ZT7a.js → cynefin-VYW2F7L2-DOSg0Q6x.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-NinMHYPB.js → cynefinDiagram-TSTJHNR4-Bm8Ct13U.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-CmFL5D4c.js → dagre-VKFMJZFB-Cnw99n0y.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-D_C52SB6.js → diagram-FQU43EPY-B742HZhK.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-BLlpvNO5.js → diagram-G47NLZAW-BGXKr2u9.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-BNT2NQnR.js → diagram-NH7WQ7WH-DIyEYN64.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-DlJ-T7tl.js → diagram-OA4YK3LP-BdRMWU80.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-D7s_sZnL.js → diagram-WEI45ONY-Bu-180k8.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-PGksmUKG.js → ebnfDiagram-CCIWWBDH-Djcej7Pq.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-CbfrT-bt.js → erDiagram-Q63AITRT-D70JxkGr.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-CqUGdXdQ.js → flowDiagram-23GEKE2U-DovWUM1i.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-DpH0ybM6.js → ganttDiagram-NO4QXBWP-Izsfqq5i.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-Cmc3LsBg.js → gitGraphDiagram-IHSO6WYX-BTZo0N08.js} +1 -1
  35. package/dist/client/assets/index-BE4Js_ss.js +456 -0
  36. package/dist/client/assets/index-CRIZk117.css +1 -0
  37. package/dist/client/assets/{infoDiagram-FWYZ7A6U-CHzl_SDV.js → infoDiagram-FWYZ7A6U-Ina5uVvZ.js} +1 -1
  38. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-BUVwtCaN.js → ishikawaDiagram-FXEZZL3T-iUdzuACl.js} +1 -1
  39. package/dist/client/assets/{journeyDiagram-5HDEW3XC-DrmO4THP.js → journeyDiagram-5HDEW3XC-lh15NShH.js} +1 -1
  40. package/dist/client/assets/{kanban-definition-HUTT4EX6-DTZoUd1n.js → kanban-definition-HUTT4EX6-CKZooxMa.js} +1 -1
  41. package/dist/client/assets/{linear-DgCNjduP.js → linear-BrmrQdSX.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-DLidsBlx.js → mermaid.core-fbY4nFyi.js} +4 -4
  43. package/dist/client/assets/{mindmap-definition-LN4V7U3C-srFG-Dae.js → mindmap-definition-LN4V7U3C-BHUhjMUf.js} +1 -1
  44. package/dist/client/assets/{pegDiagram-2B236MQR-Qary-lxo.js → pegDiagram-2B236MQR-8qcHqxsE.js} +1 -1
  45. package/dist/client/assets/{pieDiagram-ENE6RG2P-D924FFIQ.js → pieDiagram-ENE6RG2P-CpIkkExH.js} +1 -1
  46. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-YFBMJKUv.js → quadrantDiagram-ABIIQ3AL-hWzvYsWG.js} +1 -1
  47. package/dist/client/assets/{railroadDiagram-RFXS5EU6-C__7fBPw.js → railroadDiagram-RFXS5EU6-2nhrE9bK.js} +1 -1
  48. package/dist/client/assets/{requirementDiagram-TGXJPOKE-BtAq5f69.js → requirementDiagram-TGXJPOKE-B05mEN7w.js} +1 -1
  49. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-BGULYk91.js → sankeyDiagram-HTMAVEWB-CXHkBpDN.js} +1 -1
  50. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BBjCjIFm.js → sequenceDiagram-DBY2YBRQ-C4wG1r_I.js} +1 -1
  51. package/dist/client/assets/{sizeCapture-X5ZJPWSS-DjP0UZy6.js → sizeCapture-X5ZJPWSS-D_3i42tQ.js} +1 -1
  52. package/dist/client/assets/{stateDiagram-2N3HPSRC-BTqu5zVt.js → stateDiagram-2N3HPSRC-DhEgczRA.js} +1 -1
  53. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-D9gqRttI.js +1 -0
  54. package/dist/client/assets/{swimlanes-5IMT3BWC-ClySuTz2.js → swimlanes-5IMT3BWC-DTrpFviv.js} +2 -2
  55. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CNOSFfLB.js +8 -0
  56. package/dist/client/assets/{timeline-definition-FHXFAJF6-DHURyh7H.js → timeline-definition-FHXFAJF6-DLpl_8Za.js} +1 -1
  57. package/dist/client/assets/{vennDiagram-L72KCM5P-BFdn5KML.js → vennDiagram-L72KCM5P-CgnCFHj7.js} +1 -1
  58. package/dist/client/assets/{wardleyDiagram-EHGQE667-DKEHwCwO.js → wardleyDiagram-EHGQE667-BwNfD-cz.js} +1 -1
  59. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CI5HzOMq.js → xychartDiagram-FW5EYKEG--sZgo_Fw.js} +1 -1
  60. package/dist/client/index.html +2 -2
  61. package/dist/client/sw.js +1 -1
  62. package/dist/server/agent/automationStore.js +12 -7
  63. package/dist/server/agent/sessionSearchStore.js +135 -5
  64. package/dist/server/entry.js +24 -1
  65. package/dist/server/routes/terminal.js +6 -2
  66. package/dist/server/utils/runtimeMonitor.js +287 -0
  67. package/dist/server/utils/settings.js +9 -0
  68. package/package.json +1 -1
  69. package/runtime-manifest.json +2 -2
  70. package/dist/client/assets/channel-D1KecDCp.js +0 -1
  71. package/dist/client/assets/classDiagram-OUVF2IWQ-BYJ3p4nE.js +0 -1
  72. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BYJ3p4nE.js +0 -1
  73. package/dist/client/assets/index-BwMQSNQq.js +0 -456
  74. package/dist/client/assets/index-RGd2Qu1X.css +0 -1
  75. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-D5FsphfH.js +0 -1
  76. package/dist/client/assets/swimlanesDiagram-G3AALYLV-C1_CRJSE.js +0 -8
@@ -1,4 +1,4 @@
1
- import { app, BrowserWindow, dialog, ipcMain, Menu, net, Notification, session, shell, } from 'electron';
1
+ import { app, BrowserWindow, dialog, ipcMain, Menu, net, nativeImage, Notification, screen, session, shell, Tray, } from 'electron';
2
2
  import { execFile, spawn } from 'node:child_process';
3
3
  import crypto from 'node:crypto';
4
4
  import dgram from 'node:dgram';
@@ -8,6 +8,7 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { promisify } from 'node:util';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { desktopStatusTooltip, formatCompactDesktopStatus, mergeServiceActivity, nextServiceOrigin, normalizeServiceActivity, summarizeServiceActivity, } from './activityStatus.js';
11
12
  import { checkForDesktopUpdates, configureDesktopUpdater, ensureLatestRuntime, getDesktopUpdateState, installDownloadedDesktopUpdate, subscribeDesktopUpdateState, } from './updater.js';
12
13
  import { resolvePackagedRuntime, rollbackDownloadedRuntime, } from './runtime.js';
13
14
  import { isExternalLinkStagingUrl, isSafeExternalUrl } from './externalLinks.js';
@@ -52,8 +53,12 @@ function triggerLocalNetworkPermission() {
52
53
  let mainWindow = null;
53
54
  const serviceWindows = new Map();
54
55
  const windowServiceOrigins = new WeakMap();
55
- const serviceActivity = new Map();
56
+ const reportedServiceActivity = new Map();
57
+ const observedServiceActivity = new Map();
56
58
  let lastFocusedServiceWindow = null;
59
+ let menuBarStatus = null;
60
+ let floatingWidgetWindow = null;
61
+ let floatingPositionTimer = null;
57
62
  let isQuitting = false;
58
63
  function focusedWorkspaceWindow() {
59
64
  const focused = BrowserWindow.getFocusedWindow();
@@ -77,7 +82,7 @@ function serviceActivitySnapshot(targetWindow) {
77
82
  return [...serviceWindows.entries()].flatMap(([origin, window]) => {
78
83
  if (window.isDestroyed())
79
84
  return [];
80
- const activity = serviceActivity.get(origin) ?? { runningCount: 0, reviewCount: 0 };
85
+ const activity = mergeServiceActivity(reportedServiceActivity.get(origin), observedServiceActivity.get(origin));
81
86
  return [{
82
87
  origin,
83
88
  label: serviceLabel(origin),
@@ -93,6 +98,7 @@ function broadcastServiceActivity() {
93
98
  continue;
94
99
  window.webContents.send('desktop:service-activity-changed', serviceActivitySnapshot(window));
95
100
  }
101
+ refreshDesktopStatusSurfaces();
96
102
  }
97
103
  function serviceLabel(url) {
98
104
  const normalized = normalizeServiceUrl(url);
@@ -104,6 +110,223 @@ function serviceLabel(url) {
104
110
  ? '本机'
105
111
  : parsed.host;
106
112
  }
113
+ function allServiceActivity() {
114
+ const focused = BrowserWindow.getFocusedWindow();
115
+ return [...serviceWindows.entries()].flatMap(([origin, window]) => {
116
+ if (window.isDestroyed())
117
+ return [];
118
+ return [{
119
+ origin,
120
+ label: serviceLabel(origin),
121
+ current: window === lastFocusedServiceWindow,
122
+ focused: window === focused,
123
+ ...mergeServiceActivity(reportedServiceActivity.get(origin), observedServiceActivity.get(origin)),
124
+ }];
125
+ });
126
+ }
127
+ function desktopStatusSnapshot() {
128
+ const services = allServiceActivity();
129
+ const summary = summarizeServiceActivity(services);
130
+ return {
131
+ ...summary,
132
+ text: formatCompactDesktopStatus(summary),
133
+ tooltip: desktopStatusTooltip(summary),
134
+ services,
135
+ preferences: readDesktopConfig().desktopPreferences,
136
+ };
137
+ }
138
+ function nextServiceWindow(scope) {
139
+ const currentOrigin = lastFocusedServiceWindow && !lastFocusedServiceWindow.isDestroyed()
140
+ ? windowServiceOrigins.get(lastFocusedServiceWindow) ?? null
141
+ : null;
142
+ const origin = nextServiceOrigin(allServiceActivity(), currentOrigin, scope);
143
+ return origin ? serviceWindows.get(origin) ?? null : focusedWorkspaceWindow();
144
+ }
145
+ function focusNextService(scope = 'attention') {
146
+ const target = nextServiceWindow(scope);
147
+ if (target && !target.isDestroyed()) {
148
+ showAndFocusWindow(target);
149
+ return;
150
+ }
151
+ void showConnectionCenter();
152
+ }
153
+ function menuBarContextMenu() {
154
+ const status = desktopStatusSnapshot();
155
+ const serviceItems = status.services.length > 0
156
+ ? status.services
157
+ .sort((left, right) => right.reviewCount - left.reviewCount
158
+ || right.runningCount - left.runningCount
159
+ || left.label.localeCompare(right.label))
160
+ .map((service) => ({
161
+ label: `${service.label}${service.runningCount > 0 ? ` 运行 ${service.runningCount}` : ''}${service.reviewCount > 0 ? ` 待办 ${service.reviewCount}` : ''}`,
162
+ click: () => {
163
+ const target = serviceWindows.get(service.origin);
164
+ if (target && !target.isDestroyed())
165
+ showAndFocusWindow(target);
166
+ },
167
+ }))
168
+ : [{ label: '暂无已打开服务', enabled: false }];
169
+ return Menu.buildFromTemplate([
170
+ { label: status.tooltip, enabled: false },
171
+ { type: 'separator' },
172
+ ...serviceItems,
173
+ { type: 'separator' },
174
+ { label: '连接中心…', click: () => void showConnectionCenter() },
175
+ {
176
+ label: '显示悬浮状态',
177
+ type: 'checkbox',
178
+ checked: status.preferences.floatingWidgetEnabled,
179
+ click: (item) => updateDesktopPreferences({ floatingWidgetEnabled: item.checked }),
180
+ },
181
+ { type: 'separator' },
182
+ { role: 'quit' },
183
+ ]);
184
+ }
185
+ function refreshMenuBarStatus(status) {
186
+ if (process.platform !== 'darwin' || !status.preferences.menuBarStatusEnabled) {
187
+ menuBarStatus?.destroy();
188
+ menuBarStatus = null;
189
+ return;
190
+ }
191
+ if (!menuBarStatus || menuBarStatus.isDestroyed()) {
192
+ // An empty NativeImage creates a title-only NSStatusItem: no app icon and
193
+ // no icon-width spacer, which is the compact mode requested by users.
194
+ menuBarStatus = new Tray(nativeImage.createEmpty());
195
+ menuBarStatus.on('click', () => focusNextService('attention'));
196
+ menuBarStatus.on('right-click', () => menuBarStatus?.popUpContextMenu(menuBarContextMenu()));
197
+ }
198
+ menuBarStatus.setTitle(status.text);
199
+ menuBarStatus.setToolTip(`${status.tooltip}\n点击循环切换需关注的服务`);
200
+ }
201
+ function constrainFloatingPosition(position) {
202
+ const width = 212;
203
+ const height = 50;
204
+ const display = position
205
+ ? screen.getDisplayMatching({ x: position.x, y: position.y, width, height })
206
+ : screen.getPrimaryDisplay();
207
+ const area = display.workArea;
208
+ const preferred = position ?? {
209
+ x: area.x + area.width - width - 18,
210
+ y: area.y + 18,
211
+ };
212
+ return {
213
+ x: Math.min(area.x + area.width - width, Math.max(area.x, Math.round(preferred.x))),
214
+ y: Math.min(area.y + area.height - height, Math.max(area.y, Math.round(preferred.y))),
215
+ };
216
+ }
217
+ function createFloatingWidget(position) {
218
+ const bounds = constrainFloatingPosition(position);
219
+ const window = new BrowserWindow({
220
+ title: 'Termdock Status',
221
+ type: 'panel',
222
+ width: 212,
223
+ height: 50,
224
+ x: bounds.x,
225
+ y: bounds.y,
226
+ show: false,
227
+ frame: false,
228
+ transparent: true,
229
+ backgroundColor: 'rgba(0, 0, 0, 0)',
230
+ vibrancy: 'hud',
231
+ visualEffectState: 'active',
232
+ resizable: false,
233
+ maximizable: false,
234
+ minimizable: false,
235
+ fullscreenable: false,
236
+ skipTaskbar: true,
237
+ alwaysOnTop: true,
238
+ acceptFirstMouse: true,
239
+ hasShadow: true,
240
+ webPreferences: {
241
+ preload: path.join(currentDir, 'preload.cjs'),
242
+ nodeIntegration: false,
243
+ contextIsolation: true,
244
+ sandbox: true,
245
+ backgroundThrottling: false,
246
+ },
247
+ });
248
+ window.setAlwaysOnTop(true, 'floating');
249
+ window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
250
+ // Keep the auxiliary status panel out of App Expose / Mission Control so it
251
+ // cannot disrupt macOS's three-finger application-window gesture.
252
+ window.setHiddenInMissionControl(true);
253
+ let constrainingMove = false;
254
+ window.on('move', () => {
255
+ if (constrainingMove || window.isDestroyed())
256
+ return;
257
+ const [x, y] = window.getPosition();
258
+ const constrained = constrainFloatingPosition({ x, y });
259
+ if (constrained.x === x && constrained.y === y)
260
+ return;
261
+ constrainingMove = true;
262
+ window.setPosition(constrained.x, constrained.y, false);
263
+ constrainingMove = false;
264
+ });
265
+ window.on('moved', () => {
266
+ if (floatingPositionTimer)
267
+ clearTimeout(floatingPositionTimer);
268
+ floatingPositionTimer = setTimeout(() => {
269
+ floatingPositionTimer = null;
270
+ if (window.isDestroyed())
271
+ return;
272
+ const [x, y] = window.getPosition();
273
+ const config = readDesktopConfig();
274
+ config.desktopPreferences.floatingWidgetPosition = { x, y };
275
+ writeDesktopConfig(config);
276
+ }, 250);
277
+ });
278
+ window.on('closed', () => {
279
+ if (floatingWidgetWindow === window)
280
+ floatingWidgetWindow = null;
281
+ });
282
+ const rendererPath = app.isPackaged
283
+ ? path.join(process.resourcesPath, 'renderer', 'floating.html')
284
+ : path.join(projectRoot, 'desktop', 'renderer', 'floating.html');
285
+ void window.loadFile(rendererPath).then(() => window.showInactive());
286
+ return window;
287
+ }
288
+ function refreshFloatingWidget(status) {
289
+ if (process.platform !== 'darwin' || !status.preferences.floatingWidgetEnabled) {
290
+ if (floatingWidgetWindow && !floatingWidgetWindow.isDestroyed())
291
+ floatingWidgetWindow.destroy();
292
+ floatingWidgetWindow = null;
293
+ return;
294
+ }
295
+ if (!floatingWidgetWindow || floatingWidgetWindow.isDestroyed()) {
296
+ floatingWidgetWindow = createFloatingWidget(status.preferences.floatingWidgetPosition);
297
+ }
298
+ }
299
+ function keepFloatingWidgetOnScreen() {
300
+ if (!floatingWidgetWindow || floatingWidgetWindow.isDestroyed())
301
+ return;
302
+ const [x, y] = floatingWidgetWindow.getPosition();
303
+ const constrained = constrainFloatingPosition({ x, y });
304
+ if (constrained.x !== x || constrained.y !== y) {
305
+ floatingWidgetWindow.setPosition(constrained.x, constrained.y, false);
306
+ }
307
+ }
308
+ function refreshDesktopStatusSurfaces() {
309
+ if (!app.isReady())
310
+ return;
311
+ const status = desktopStatusSnapshot();
312
+ refreshMenuBarStatus(status);
313
+ refreshFloatingWidget(status);
314
+ for (const window of BrowserWindow.getAllWindows()) {
315
+ if (!window.isDestroyed())
316
+ window.webContents.send('desktop:status-changed', status);
317
+ }
318
+ }
319
+ function updateDesktopPreferences(patch) {
320
+ const config = readDesktopConfig();
321
+ if (typeof patch.menuBarStatusEnabled === 'boolean') {
322
+ config.desktopPreferences.menuBarStatusEnabled = patch.menuBarStatusEnabled;
323
+ }
324
+ if (typeof patch.floatingWidgetEnabled === 'boolean') {
325
+ config.desktopPreferences.floatingWidgetEnabled = patch.floatingWidgetEnabled;
326
+ }
327
+ writeDesktopConfig(config);
328
+ refreshDesktopStatusSurfaces();
329
+ }
107
330
  function showDesktopMessageBox(options) {
108
331
  const parent = BrowserWindow.getFocusedWindow() ?? mainWindow;
109
332
  return parent
@@ -157,6 +380,28 @@ function defaultConfig() {
157
380
  connections: [],
158
381
  lastConnectionUrl: null,
159
382
  trustedCertificateAuthorities: [],
383
+ desktopPreferences: {
384
+ menuBarStatusEnabled: true,
385
+ floatingWidgetEnabled: false,
386
+ floatingWidgetPosition: null,
387
+ },
388
+ };
389
+ }
390
+ function normalizeDesktopPreferences(value) {
391
+ const input = value && typeof value === 'object'
392
+ ? value
393
+ : {};
394
+ const position = input.floatingWidgetPosition;
395
+ return {
396
+ // The compact, text-only status is the useful default on macOS. Existing
397
+ // desktop.json files therefore gain the feature without a migration step.
398
+ menuBarStatusEnabled: input.menuBarStatusEnabled !== false,
399
+ floatingWidgetEnabled: input.floatingWidgetEnabled === true,
400
+ floatingWidgetPosition: position
401
+ && Number.isFinite(position.x)
402
+ && Number.isFinite(position.y)
403
+ ? { x: Math.round(position.x), y: Math.round(position.y) }
404
+ : null,
160
405
  };
161
406
  }
162
407
  function normalizeServiceUrl(raw) {
@@ -286,6 +531,7 @@ function readDesktopConfig() {
286
531
  && typeof entry.subject === 'string'
287
532
  && typeof entry.trustedAt === 'number')
288
533
  : [],
534
+ desktopPreferences: normalizeDesktopPreferences(parsed.desktopPreferences),
289
535
  };
290
536
  }
291
537
  catch {
@@ -616,6 +862,7 @@ async function snapshot() {
616
862
  localService,
617
863
  connections: config.connections,
618
864
  lastConnectionUrl: config.lastConnectionUrl,
865
+ desktopPreferences: config.desktopPreferences,
619
866
  };
620
867
  }
621
868
  function desktopRuntimeEnv(runtime = runtimePaths()) {
@@ -830,6 +1077,7 @@ async function connectWindow(rawUrl) {
830
1077
  }
831
1078
  const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
832
1079
  serviceWindows.set(key, workspaceWindow);
1080
+ broadcastServiceActivity();
833
1081
  while (!workspaceWindow.isDestroyed()) {
834
1082
  try {
835
1083
  await workspaceWindow.loadURL(probe.url);
@@ -1030,7 +1278,8 @@ function createDesktopWindow(options) {
1030
1278
  const serviceOrigin = windowServiceOrigins.get(window);
1031
1279
  if (serviceOrigin && serviceWindows.get(serviceOrigin) === window) {
1032
1280
  serviceWindows.delete(serviceOrigin);
1033
- serviceActivity.delete(serviceOrigin);
1281
+ reportedServiceActivity.delete(serviceOrigin);
1282
+ observedServiceActivity.delete(serviceOrigin);
1034
1283
  broadcastServiceActivity();
1035
1284
  }
1036
1285
  });
@@ -1085,18 +1334,28 @@ function installIpcHandlers() {
1085
1334
  const origin = windowServiceOrigins.get(sourceWindow);
1086
1335
  if (!origin)
1087
1336
  return;
1088
- const normalizeCount = (value) => Math.min(999, Math.max(0, typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0));
1089
- const next = {
1090
- runningCount: normalizeCount(activity?.runningCount),
1091
- reviewCount: normalizeCount(activity?.reviewCount),
1092
- };
1093
- const current = serviceActivity.get(origin);
1337
+ const next = normalizeServiceActivity(activity);
1338
+ const current = reportedServiceActivity.get(origin);
1094
1339
  if (current?.runningCount === next.runningCount && current.reviewCount === next.reviewCount) {
1095
1340
  // The sender may have just reloaded and still needs the complete roster.
1096
1341
  sourceWindow.webContents.send('desktop:service-activity-changed', serviceActivitySnapshot(sourceWindow));
1097
1342
  return;
1098
1343
  }
1099
- serviceActivity.set(origin, next);
1344
+ reportedServiceActivity.set(origin, next);
1345
+ broadcastServiceActivity();
1346
+ });
1347
+ ipcMain.on('desktop:observe-service-activity', (event, activity) => {
1348
+ const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1349
+ if (!sourceWindow)
1350
+ return;
1351
+ const origin = windowServiceOrigins.get(sourceWindow);
1352
+ if (!origin)
1353
+ return;
1354
+ const next = normalizeServiceActivity(activity);
1355
+ const current = observedServiceActivity.get(origin);
1356
+ if (current?.runningCount === next.runningCount && current.reviewCount === next.reviewCount)
1357
+ return;
1358
+ observedServiceActivity.set(origin, next);
1100
1359
  broadcastServiceActivity();
1101
1360
  });
1102
1361
  ipcMain.handle('desktop:focus-service', (event, origin) => {
@@ -1111,6 +1370,30 @@ function installIpcHandlers() {
1111
1370
  return true;
1112
1371
  });
1113
1372
  ipcMain.handle('desktop:start-local', () => startLocalService());
1373
+ ipcMain.handle('desktop:update-preferences', async (_event, preferences) => {
1374
+ updateDesktopPreferences({
1375
+ ...(typeof preferences?.menuBarStatusEnabled === 'boolean'
1376
+ ? { menuBarStatusEnabled: preferences.menuBarStatusEnabled }
1377
+ : {}),
1378
+ ...(typeof preferences?.floatingWidgetEnabled === 'boolean'
1379
+ ? { floatingWidgetEnabled: preferences.floatingWidgetEnabled }
1380
+ : {}),
1381
+ });
1382
+ return snapshot();
1383
+ });
1384
+ ipcMain.handle('desktop:status-snapshot', () => desktopStatusSnapshot());
1385
+ ipcMain.handle('desktop:focus-next-service', (_event, scope) => {
1386
+ const normalized = scope === 'running'
1387
+ || scope === 'review'
1388
+ || scope === 'all'
1389
+ ? scope
1390
+ : 'attention';
1391
+ focusNextService(normalized);
1392
+ });
1393
+ ipcMain.handle('desktop:disable-floating-widget', async () => {
1394
+ updateDesktopPreferences({ floatingWidgetEnabled: false });
1395
+ return snapshot();
1396
+ });
1114
1397
  ipcMain.handle('desktop:install-cli', () => installCli());
1115
1398
  ipcMain.handle('desktop:update-state', () => getDesktopUpdateState());
1116
1399
  ipcMain.handle('desktop:check-update', () => checkForDesktopUpdates());
@@ -1388,6 +1671,9 @@ app.whenReady().then(async () => {
1388
1671
  configureLocalServiceCertificateTrust();
1389
1672
  installIpcHandlers();
1390
1673
  installMenu();
1674
+ refreshDesktopStatusSurfaces();
1675
+ screen.on('display-removed', keepFloatingWidgetOnScreen);
1676
+ screen.on('display-metrics-changed', keepFloatingWidgetOnScreen);
1391
1677
  configureDesktopUpdater(showDesktopMessageBox);
1392
1678
  subscribeDesktopUpdateState((state) => {
1393
1679
  for (const window of BrowserWindow.getAllWindows()) {