termdock 1.4.107 → 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 (79) hide show
  1. package/.desktop-dist/main.js +345 -1
  2. package/dist/client/assets/{ModelPreview-CoR41z2e.js → ModelPreview-Cxk4IZLv.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-CXFrMEcU.js → abnfDiagram-VRR7QNED-Bz2HWRRX.js} +1 -1
  4. package/dist/client/assets/{arc-DXT1yvki.js → arc-D3vf35f_.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-B4Tl5gm4.js → architectureDiagram-ZJ3FMSHR-LI-OXsDp.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-D8OY0bG2.js → blockDiagram-677ZJIJ3-Dm1JtLIo.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-BHIhcBy3.js → c4Diagram-LMCZKHZV-DSkxlBnU.js} +1 -1
  8. package/dist/client/assets/channel-qZtjoGxW.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-NFF-sZCT.js → chunk-2Q5K7J3B-Wd2AO6yQ.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-Cysd2WNQ.js → chunk-32BRIVSS-CIBkFIAg.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-BRHAAr29.js → chunk-5VM5RSS4-9veDV0NJ.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-BovDZYVQ.js → chunk-EX3LRPZG-DVi5mqrV.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-ByWUvKGO.js → chunk-JWPE2WC7-DZc_3_5i.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-GWk63RII.js → chunk-MOJQB5TN-Czq1YGEA.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-GvzHMCqG.js → chunk-RYQCIY6F-BN3UqH_7.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-BoiSCBad.js → chunk-V7JOEXUC-BgDCEp_z.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-ByS_rjkq.js → chunk-VR4S4FIN-BnyRVZSn.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-D9fRuB0M.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-BVOwkeRc.js → cose-bilkent-JH36ORCC-B-P2BstB.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-8rNTW_mJ.js → cynefin-VYW2F7L2-DOSg0Q6x.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DHASHiYD.js → cynefinDiagram-TSTJHNR4-Bm8Ct13U.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-wWqVXFNE.js → dagre-VKFMJZFB-Cnw99n0y.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-Bp2E7omd.js → diagram-FQU43EPY-B742HZhK.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-DajrBIg7.js → diagram-G47NLZAW-BGXKr2u9.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-ByGTRgNg.js → diagram-NH7WQ7WH-DIyEYN64.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-bsRCtWPe.js → diagram-OA4YK3LP-BdRMWU80.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-9f1_0Bjd.js → diagram-WEI45ONY-Bu-180k8.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DyvJDQQ5.js → ebnfDiagram-CCIWWBDH-Djcej7Pq.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-DwLHRiMz.js → erDiagram-Q63AITRT-D70JxkGr.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U--9lOgA3z.js → flowDiagram-23GEKE2U-DovWUM1i.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-j9C0N_DS.js → ganttDiagram-NO4QXBWP-Izsfqq5i.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-lHdVffsh.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-D8kAsmdn.js → infoDiagram-FWYZ7A6U-Ina5uVvZ.js} +1 -1
  38. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CTY9BLyE.js → ishikawaDiagram-FXEZZL3T-iUdzuACl.js} +1 -1
  39. package/dist/client/assets/{journeyDiagram-5HDEW3XC-B3QO8wF2.js → journeyDiagram-5HDEW3XC-lh15NShH.js} +1 -1
  40. package/dist/client/assets/{kanban-definition-HUTT4EX6-CesXysl2.js → kanban-definition-HUTT4EX6-CKZooxMa.js} +1 -1
  41. package/dist/client/assets/{linear-wxKR9xNu.js → linear-BrmrQdSX.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-DqsMKQ0n.js → mermaid.core-fbY4nFyi.js} +4 -4
  43. package/dist/client/assets/{mindmap-definition-LN4V7U3C-CCfpEZEP.js → mindmap-definition-LN4V7U3C-BHUhjMUf.js} +1 -1
  44. package/dist/client/assets/{pegDiagram-2B236MQR-DEje4xdU.js → pegDiagram-2B236MQR-8qcHqxsE.js} +1 -1
  45. package/dist/client/assets/{pieDiagram-ENE6RG2P-B5guqQ7K.js → pieDiagram-ENE6RG2P-CpIkkExH.js} +1 -1
  46. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-D58kwRrP.js → quadrantDiagram-ABIIQ3AL-hWzvYsWG.js} +1 -1
  47. package/dist/client/assets/{railroadDiagram-RFXS5EU6-D9VFfFu0.js → railroadDiagram-RFXS5EU6-2nhrE9bK.js} +1 -1
  48. package/dist/client/assets/{requirementDiagram-TGXJPOKE-B0q4l0cr.js → requirementDiagram-TGXJPOKE-B05mEN7w.js} +1 -1
  49. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-DTsXXi0M.js → sankeyDiagram-HTMAVEWB-CXHkBpDN.js} +1 -1
  50. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-3Xpi1nmi.js → sequenceDiagram-DBY2YBRQ-C4wG1r_I.js} +1 -1
  51. package/dist/client/assets/{sizeCapture-X5ZJPWSS-C0jHd0eC.js → sizeCapture-X5ZJPWSS-D_3i42tQ.js} +1 -1
  52. package/dist/client/assets/{stateDiagram-2N3HPSRC-DlDm_dYg.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-C128cuo6.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-BHsLELm-.js → timeline-definition-FHXFAJF6-DLpl_8Za.js} +1 -1
  57. package/dist/client/assets/{vennDiagram-L72KCM5P-DkdQFPIy.js → vennDiagram-L72KCM5P-CgnCFHj7.js} +1 -1
  58. package/dist/client/assets/{wardleyDiagram-EHGQE667-DWaahbD5.js → wardleyDiagram-EHGQE667-BwNfD-cz.js} +1 -1
  59. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CSwOYmgm.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/filesystem.js +31 -6
  66. package/dist/server/routes/terminal.js +6 -2
  67. package/dist/server/utils/heicPreview.js +50 -0
  68. package/dist/server/utils/runtimeMonitor.js +287 -0
  69. package/dist/server/utils/settings.js +9 -0
  70. package/dist/server/workers/heicPreviewWorker.js +14 -0
  71. package/package.json +3 -1
  72. package/runtime-manifest.json +3 -3
  73. package/dist/client/assets/channel-CXw70OW3.js +0 -1
  74. package/dist/client/assets/classDiagram-OUVF2IWQ-CNF7sVVd.js +0 -1
  75. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CNF7sVVd.js +0 -1
  76. package/dist/client/assets/index-B1FKcn-W.js +0 -451
  77. package/dist/client/assets/index-BloZ2WQg.css +0 -1
  78. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVOBevpU.js +0 -1
  79. package/dist/client/assets/swimlanesDiagram-G3AALYLV-Bf87D8dr.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,7 +53,12 @@ function triggerLocalNetworkPermission() {
52
53
  let mainWindow = null;
53
54
  const serviceWindows = new Map();
54
55
  const windowServiceOrigins = new WeakMap();
56
+ const reportedServiceActivity = new Map();
57
+ const observedServiceActivity = new Map();
55
58
  let lastFocusedServiceWindow = null;
59
+ let menuBarStatus = null;
60
+ let floatingWidgetWindow = null;
61
+ let floatingPositionTimer = null;
56
62
  let isQuitting = false;
57
63
  function focusedWorkspaceWindow() {
58
64
  const focused = BrowserWindow.getFocusedWindow();
@@ -71,6 +77,29 @@ function showAndFocusWindow(window) {
71
77
  window.show();
72
78
  window.focus();
73
79
  }
80
+ function serviceActivitySnapshot(targetWindow) {
81
+ const currentOrigin = windowServiceOrigins.get(targetWindow);
82
+ return [...serviceWindows.entries()].flatMap(([origin, window]) => {
83
+ if (window.isDestroyed())
84
+ return [];
85
+ const activity = mergeServiceActivity(reportedServiceActivity.get(origin), observedServiceActivity.get(origin));
86
+ return [{
87
+ origin,
88
+ label: serviceLabel(origin),
89
+ current: origin === currentOrigin,
90
+ focused: window.isFocused(),
91
+ ...activity,
92
+ }];
93
+ });
94
+ }
95
+ function broadcastServiceActivity() {
96
+ for (const window of serviceWindows.values()) {
97
+ if (window.isDestroyed())
98
+ continue;
99
+ window.webContents.send('desktop:service-activity-changed', serviceActivitySnapshot(window));
100
+ }
101
+ refreshDesktopStatusSurfaces();
102
+ }
74
103
  function serviceLabel(url) {
75
104
  const normalized = normalizeServiceUrl(url);
76
105
  const saved = readDesktopConfig().connections.find((entry) => entry.url === normalized);
@@ -81,6 +110,223 @@ function serviceLabel(url) {
81
110
  ? '本机'
82
111
  : parsed.host;
83
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
+ }
84
330
  function showDesktopMessageBox(options) {
85
331
  const parent = BrowserWindow.getFocusedWindow() ?? mainWindow;
86
332
  return parent
@@ -134,6 +380,28 @@ function defaultConfig() {
134
380
  connections: [],
135
381
  lastConnectionUrl: null,
136
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,
137
405
  };
138
406
  }
139
407
  function normalizeServiceUrl(raw) {
@@ -263,6 +531,7 @@ function readDesktopConfig() {
263
531
  && typeof entry.subject === 'string'
264
532
  && typeof entry.trustedAt === 'number')
265
533
  : [],
534
+ desktopPreferences: normalizeDesktopPreferences(parsed.desktopPreferences),
266
535
  };
267
536
  }
268
537
  catch {
@@ -593,6 +862,7 @@ async function snapshot() {
593
862
  localService,
594
863
  connections: config.connections,
595
864
  lastConnectionUrl: config.lastConnectionUrl,
865
+ desktopPreferences: config.desktopPreferences,
596
866
  };
597
867
  }
598
868
  function desktopRuntimeEnv(runtime = runtimePaths()) {
@@ -807,6 +1077,7 @@ async function connectWindow(rawUrl) {
807
1077
  }
808
1078
  const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
809
1079
  serviceWindows.set(key, workspaceWindow);
1080
+ broadcastServiceActivity();
810
1081
  while (!workspaceWindow.isDestroyed()) {
811
1082
  try {
812
1083
  await workspaceWindow.loadURL(probe.url);
@@ -1007,11 +1278,15 @@ function createDesktopWindow(options) {
1007
1278
  const serviceOrigin = windowServiceOrigins.get(window);
1008
1279
  if (serviceOrigin && serviceWindows.get(serviceOrigin) === window) {
1009
1280
  serviceWindows.delete(serviceOrigin);
1281
+ reportedServiceActivity.delete(serviceOrigin);
1282
+ observedServiceActivity.delete(serviceOrigin);
1283
+ broadcastServiceActivity();
1010
1284
  }
1011
1285
  });
1012
1286
  window.on('focus', () => {
1013
1287
  if (windowServiceOrigins.has(window))
1014
1288
  lastFocusedServiceWindow = window;
1289
+ broadcastServiceActivity();
1015
1290
  // User is looking at the app — the Dock badge has served its purpose.
1016
1291
  clearUnreadNotifications();
1017
1292
  });
@@ -1052,7 +1327,73 @@ function installIpcHandlers() {
1052
1327
  return snapshot();
1053
1328
  });
1054
1329
  ipcMain.handle('desktop:connect', (_event, url) => connectWindow(url));
1330
+ ipcMain.on('desktop:report-service-activity', (event, activity) => {
1331
+ const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1332
+ if (!sourceWindow)
1333
+ return;
1334
+ const origin = windowServiceOrigins.get(sourceWindow);
1335
+ if (!origin)
1336
+ return;
1337
+ const next = normalizeServiceActivity(activity);
1338
+ const current = reportedServiceActivity.get(origin);
1339
+ if (current?.runningCount === next.runningCount && current.reviewCount === next.reviewCount) {
1340
+ // The sender may have just reloaded and still needs the complete roster.
1341
+ sourceWindow.webContents.send('desktop:service-activity-changed', serviceActivitySnapshot(sourceWindow));
1342
+ return;
1343
+ }
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);
1359
+ broadcastServiceActivity();
1360
+ });
1361
+ ipcMain.handle('desktop:focus-service', (event, origin) => {
1362
+ const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1363
+ if (!sourceWindow || !windowServiceOrigins.has(sourceWindow))
1364
+ return false;
1365
+ const targetWindow = serviceWindows.get(origin);
1366
+ if (!targetWindow || targetWindow.isDestroyed())
1367
+ return false;
1368
+ showAndFocusWindow(targetWindow);
1369
+ broadcastServiceActivity();
1370
+ return true;
1371
+ });
1055
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
+ });
1056
1397
  ipcMain.handle('desktop:install-cli', () => installCli());
1057
1398
  ipcMain.handle('desktop:update-state', () => getDesktopUpdateState());
1058
1399
  ipcMain.handle('desktop:check-update', () => checkForDesktopUpdates());
@@ -1330,6 +1671,9 @@ app.whenReady().then(async () => {
1330
1671
  configureLocalServiceCertificateTrust();
1331
1672
  installIpcHandlers();
1332
1673
  installMenu();
1674
+ refreshDesktopStatusSurfaces();
1675
+ screen.on('display-removed', keepFloatingWidgetOnScreen);
1676
+ screen.on('display-metrics-changed', keepFloatingWidgetOnScreen);
1333
1677
  configureDesktopUpdater(showDesktopMessageBox);
1334
1678
  subscribeDesktopUpdateState((state) => {
1335
1679
  for (const window of BrowserWindow.getAllWindows()) {