termdock 1.4.109 → 1.4.110

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 (70) hide show
  1. package/.desktop-dist/main.js +200 -27
  2. package/dist/client/assets/{ModelPreview-Cxk4IZLv.js → ModelPreview-B40SLVdu.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-Bz2HWRRX.js → abnfDiagram-VRR7QNED-D7p11Ssd.js} +1 -1
  4. package/dist/client/assets/{arc-D3vf35f_.js → arc-CsmLIp7i.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-LI-OXsDp.js → architectureDiagram-ZJ3FMSHR-BjYttOye.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-Dm1JtLIo.js → blockDiagram-677ZJIJ3-BnWi8IVO.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-DSkxlBnU.js → c4Diagram-LMCZKHZV-BFm2RcKX.js} +1 -1
  8. package/dist/client/assets/channel-ifcSKqN3.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-Wd2AO6yQ.js → chunk-2Q5K7J3B-D_OKw60X.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-CIBkFIAg.js → chunk-32BRIVSS-D6k1lZV9.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-9veDV0NJ.js → chunk-5VM5RSS4-CEqvSDkW.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-DVi5mqrV.js → chunk-EX3LRPZG-Ca2Gmbvd.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-DZc_3_5i.js → chunk-JWPE2WC7-CRRvRUQB.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-Czq1YGEA.js → chunk-MOJQB5TN-CeZ5qCFm.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-BN3UqH_7.js → chunk-RYQCIY6F-Cx7sW5Rr.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-BgDCEp_z.js → chunk-V7JOEXUC-B3_grzN8.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-BnyRVZSn.js → chunk-VR4S4FIN-g73M8USU.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-KOa0rQIz.js → chunk-XXDRQBXY-9i-NV13r.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-CVu2gyed.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CVu2gyed.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-B-P2BstB.js → cose-bilkent-JH36ORCC-DGKauMxg.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-DOSg0Q6x.js → cynefin-VYW2F7L2-DKvEKvGd.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-Bm8Ct13U.js → cynefinDiagram-TSTJHNR4-CLXYMIpB.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-Cnw99n0y.js → dagre-VKFMJZFB-DFAR5nAp.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-B742HZhK.js → diagram-FQU43EPY-DeRBoyeq.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-BGXKr2u9.js → diagram-G47NLZAW-CDEAgMuP.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-DIyEYN64.js → diagram-NH7WQ7WH-4VFR2ZsJ.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-BdRMWU80.js → diagram-OA4YK3LP-DMKIgZlI.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-Bu-180k8.js → diagram-WEI45ONY-zEjxP4io.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-Djcej7Pq.js → ebnfDiagram-CCIWWBDH-BuErqR1l.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-D70JxkGr.js → erDiagram-Q63AITRT-D4nbc2T3.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-DovWUM1i.js → flowDiagram-23GEKE2U-B2MiQAI4.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-Izsfqq5i.js → ganttDiagram-NO4QXBWP-hGTX0vsS.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BTZo0N08.js → gitGraphDiagram-IHSO6WYX-BHHSVtvH.js} +1 -1
  35. package/dist/client/assets/index-CFXqRKcJ.css +1 -0
  36. package/dist/client/assets/index-D3kro0zp.js +456 -0
  37. package/dist/client/assets/{infoDiagram-FWYZ7A6U-Ina5uVvZ.js → infoDiagram-FWYZ7A6U-BZuNpsCG.js} +1 -1
  38. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-iUdzuACl.js → ishikawaDiagram-FXEZZL3T-C0KwjZIv.js} +1 -1
  39. package/dist/client/assets/{journeyDiagram-5HDEW3XC-lh15NShH.js → journeyDiagram-5HDEW3XC-mxy7-Irj.js} +1 -1
  40. package/dist/client/assets/{kanban-definition-HUTT4EX6-CKZooxMa.js → kanban-definition-HUTT4EX6-DslZKv6b.js} +1 -1
  41. package/dist/client/assets/{linear-BrmrQdSX.js → linear-BJXz3Iqq.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-fbY4nFyi.js → mermaid.core-Btv5hFOv.js} +4 -4
  43. package/dist/client/assets/{mindmap-definition-LN4V7U3C-BHUhjMUf.js → mindmap-definition-LN4V7U3C-BSbqyDyq.js} +1 -1
  44. package/dist/client/assets/{pegDiagram-2B236MQR-8qcHqxsE.js → pegDiagram-2B236MQR-L2cCu4W1.js} +1 -1
  45. package/dist/client/assets/{pieDiagram-ENE6RG2P-CpIkkExH.js → pieDiagram-ENE6RG2P-CXdawbl4.js} +1 -1
  46. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-hWzvYsWG.js → quadrantDiagram-ABIIQ3AL-CwKwBctE.js} +1 -1
  47. package/dist/client/assets/{railroadDiagram-RFXS5EU6-2nhrE9bK.js → railroadDiagram-RFXS5EU6-B6MBOnSp.js} +1 -1
  48. package/dist/client/assets/{requirementDiagram-TGXJPOKE-B05mEN7w.js → requirementDiagram-TGXJPOKE-CR9lns0Z.js} +1 -1
  49. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-CXHkBpDN.js → sankeyDiagram-HTMAVEWB-DLuhNvTd.js} +1 -1
  50. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-C4wG1r_I.js → sequenceDiagram-DBY2YBRQ-BorPhsA5.js} +1 -1
  51. package/dist/client/assets/{sizeCapture-X5ZJPWSS-D_3i42tQ.js → sizeCapture-X5ZJPWSS-DPXho3_K.js} +1 -1
  52. package/dist/client/assets/{stateDiagram-2N3HPSRC-DhEgczRA.js → stateDiagram-2N3HPSRC-DSbrRqVk.js} +1 -1
  53. package/dist/client/assets/stateDiagram-v2-6OUMAXLB--bVRWeRL.js +1 -0
  54. package/dist/client/assets/{swimlanes-5IMT3BWC-DTrpFviv.js → swimlanes-5IMT3BWC-D4A4axIk.js} +2 -2
  55. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CqWtgk0W.js +8 -0
  56. package/dist/client/assets/{timeline-definition-FHXFAJF6-DLpl_8Za.js → timeline-definition-FHXFAJF6-D4ObRKT8.js} +1 -1
  57. package/dist/client/assets/{vennDiagram-L72KCM5P-CgnCFHj7.js → vennDiagram-L72KCM5P-Bj_4-RtP.js} +1 -1
  58. package/dist/client/assets/{wardleyDiagram-EHGQE667-BwNfD-cz.js → wardleyDiagram-EHGQE667-BNvcAUoQ.js} +1 -1
  59. package/dist/client/assets/{xychartDiagram-FW5EYKEG--sZgo_Fw.js → xychartDiagram-FW5EYKEG-BfGV9sTq.js} +1 -1
  60. package/dist/client/index.html +2 -2
  61. package/dist/client/sw.js +1 -1
  62. package/package.json +1 -1
  63. package/runtime-manifest.json +1 -1
  64. package/dist/client/assets/channel-qZtjoGxW.js +0 -1
  65. package/dist/client/assets/classDiagram-OUVF2IWQ-CnWXW1tk.js +0 -1
  66. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CnWXW1tk.js +0 -1
  67. package/dist/client/assets/index-BE4Js_ss.js +0 -456
  68. package/dist/client/assets/index-CRIZk117.css +0 -1
  69. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-D9gqRttI.js +0 -1
  70. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CNOSFfLB.js +0 -8
@@ -53,13 +53,17 @@ function triggerLocalNetworkPermission() {
53
53
  let mainWindow = null;
54
54
  const serviceWindows = new Map();
55
55
  const windowServiceOrigins = new WeakMap();
56
+ const restorableServiceWindows = new WeakSet();
56
57
  const reportedServiceActivity = new Map();
57
58
  const observedServiceActivity = new Map();
58
59
  let lastFocusedServiceWindow = null;
59
60
  let menuBarStatus = null;
61
+ let menuBarStatusWidth = 0;
60
62
  let floatingWidgetWindow = null;
61
63
  let floatingPositionTimer = null;
62
64
  let isQuitting = false;
65
+ const FLOATING_WIDGET_WIDTHS = [64, 108, 152];
66
+ const FLOATING_WIDGET_HEIGHT = 40;
63
67
  function focusedWorkspaceWindow() {
64
68
  const focused = BrowserWindow.getFocusedWindow();
65
69
  if (focused && windowServiceOrigins.has(focused))
@@ -182,25 +186,90 @@ function menuBarContextMenu() {
182
186
  { role: 'quit' },
183
187
  ]);
184
188
  }
189
+ function menuBarStatusImage(status) {
190
+ const size = 18;
191
+ const scale = 2;
192
+ const bitmap = Buffer.alloc(size * scale * size * scale * 4);
193
+ const glyphs = {
194
+ '0': ['111', '101', '101', '101', '111'],
195
+ '1': ['010', '110', '010', '010', '111'],
196
+ '2': ['111', '001', '111', '100', '111'],
197
+ '3': ['111', '001', '111', '001', '111'],
198
+ '4': ['101', '101', '111', '001', '001'],
199
+ '5': ['111', '100', '111', '001', '111'],
200
+ '6': ['111', '100', '111', '101', '111'],
201
+ '7': ['111', '001', '010', '010', '010'],
202
+ '8': ['111', '101', '111', '101', '111'],
203
+ '9': ['111', '101', '111', '001', '111'],
204
+ '+': ['000', '010', '111', '010', '000'],
205
+ };
206
+ const drawPixel = (x, y, alpha = 255) => {
207
+ for (let offsetY = 0; offsetY < scale; offsetY += 1) {
208
+ for (let offsetX = 0; offsetX < scale; offsetX += 1) {
209
+ const pixelX = x * scale + offsetX;
210
+ const pixelY = y * scale + offsetY;
211
+ if (pixelX < 0 || pixelX >= size * scale || pixelY < 0 || pixelY >= size * scale)
212
+ continue;
213
+ bitmap[(pixelY * size * scale + pixelX) * 4 + 3] = alpha;
214
+ }
215
+ }
216
+ };
217
+ const drawGlyph = (value, x, y, alpha) => {
218
+ const glyph = glyphs[value > 9 ? '+' : String(value)] ?? glyphs['0'];
219
+ glyph.forEach((row, rowIndex) => {
220
+ [...row].forEach((cell, columnIndex) => {
221
+ if (cell === '1')
222
+ drawPixel(x + columnIndex, y + rowIndex, alpha);
223
+ });
224
+ });
225
+ };
226
+ // Three quiet, consistently aligned rows read much better at menu-bar size
227
+ // than squeezing two metrics beside each other. The ordering mirrors the
228
+ // floating widget: running, review, then connected services.
229
+ const rows = [
230
+ { value: status.runningCount, y: 0, icon: [[0, 0], [0, 1], [1, 1], [0, 2], [1, 2], [2, 2], [3, 2], [0, 3], [1, 3], [0, 4]] },
231
+ { value: status.reviewCount, y: 6, icon: [[2, 0], [1, 1], [3, 1], [0, 2], [4, 2], [1, 3], [3, 3], [2, 4]] },
232
+ { value: status.serviceCount, y: 12, icon: [[0, 0], [1, 0], [3, 0], [4, 0], [1, 1], [3, 1], [2, 2], [2, 3], [1, 4], [2, 4], [3, 4]] },
233
+ ];
234
+ rows.forEach(({ value, y, icon }) => {
235
+ const alpha = value > 0 ? 255 : 96;
236
+ icon.forEach(([offsetX, offsetY]) => drawPixel(1 + offsetX, y + offsetY, alpha));
237
+ drawGlyph(value, 11, y, alpha);
238
+ });
239
+ // Template images are intentionally monochrome; macOS supplies the correct
240
+ // menu-bar color for the active appearance and accessibility state.
241
+ const image = nativeImage.createFromBitmap(bitmap, {
242
+ width: size * scale,
243
+ height: size * scale,
244
+ scaleFactor: scale,
245
+ });
246
+ image.setTemplateImage(true);
247
+ return image;
248
+ }
185
249
  function refreshMenuBarStatus(status) {
186
250
  if (process.platform !== 'darwin' || !status.preferences.menuBarStatusEnabled) {
187
251
  menuBarStatus?.destroy();
188
252
  menuBarStatus = null;
253
+ menuBarStatusWidth = 0;
189
254
  return;
190
255
  }
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());
256
+ const image = menuBarStatusImage(status);
257
+ const imageWidth = image.getSize().width;
258
+ if (!menuBarStatus || menuBarStatus.isDestroyed() || menuBarStatusWidth !== imageWidth) {
259
+ menuBarStatus?.destroy();
260
+ menuBarStatus = new Tray(image);
261
+ menuBarStatusWidth = imageWidth;
195
262
  menuBarStatus.on('click', () => focusNextService('attention'));
196
263
  menuBarStatus.on('right-click', () => menuBarStatus?.popUpContextMenu(menuBarContextMenu()));
197
264
  }
198
- menuBarStatus.setTitle(status.text);
265
+ else {
266
+ menuBarStatus.setImage(image);
267
+ }
268
+ menuBarStatus.setTitle('');
199
269
  menuBarStatus.setToolTip(`${status.tooltip}\n点击循环切换需关注的服务`);
200
270
  }
201
- function constrainFloatingPosition(position) {
202
- const width = 212;
203
- const height = 50;
271
+ function constrainFloatingPosition(position, width = FLOATING_WIDGET_WIDTHS[2]) {
272
+ const height = FLOATING_WIDGET_HEIGHT;
204
273
  const display = position
205
274
  ? screen.getDisplayMatching({ x: position.x, y: position.y, width, height })
206
275
  : screen.getPrimaryDisplay();
@@ -219,20 +288,21 @@ function createFloatingWidget(position) {
219
288
  const window = new BrowserWindow({
220
289
  title: 'Termdock Status',
221
290
  type: 'panel',
222
- width: 212,
223
- height: 50,
291
+ width: FLOATING_WIDGET_WIDTHS[2],
292
+ height: FLOATING_WIDGET_HEIGHT,
224
293
  x: bounds.x,
225
294
  y: bounds.y,
226
295
  show: false,
227
296
  frame: false,
228
297
  transparent: true,
229
298
  backgroundColor: 'rgba(0, 0, 0, 0)',
230
- vibrancy: 'hud',
299
+ vibrancy: 'popover',
231
300
  visualEffectState: 'active',
232
301
  resizable: false,
233
302
  maximizable: false,
234
303
  minimizable: false,
235
304
  fullscreenable: false,
305
+ focusable: false,
236
306
  skipTaskbar: true,
237
307
  alwaysOnTop: true,
238
308
  acceptFirstMouse: true,
@@ -246,7 +316,13 @@ function createFloatingWidget(position) {
246
316
  },
247
317
  });
248
318
  window.setAlwaysOnTop(true, 'floating');
249
- window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
319
+ window.setVisibleOnAllWorkspaces(true, {
320
+ visibleOnFullScreen: true,
321
+ // Electron otherwise temporarily transforms the whole process into a UI
322
+ // element, which can hide the app menu and remove normal windows from App
323
+ // Expose while this panel exists.
324
+ skipTransformProcessType: true,
325
+ });
250
326
  // Keep the auxiliary status panel out of App Expose / Mission Control so it
251
327
  // cannot disrupt macOS's three-finger application-window gesture.
252
328
  window.setHiddenInMissionControl(true);
@@ -255,7 +331,8 @@ function createFloatingWidget(position) {
255
331
  if (constrainingMove || window.isDestroyed())
256
332
  return;
257
333
  const [x, y] = window.getPosition();
258
- const constrained = constrainFloatingPosition({ x, y });
334
+ const [width] = window.getSize();
335
+ const constrained = constrainFloatingPosition({ x, y }, width);
259
336
  if (constrained.x === x && constrained.y === y)
260
337
  return;
261
338
  constrainingMove = true;
@@ -285,6 +362,20 @@ function createFloatingWidget(position) {
285
362
  void window.loadFile(rendererPath).then(() => window.showInactive());
286
363
  return window;
287
364
  }
365
+ function resizeFloatingWidget(metricCount) {
366
+ const window = floatingWidgetWindow;
367
+ if (!window || window.isDestroyed())
368
+ return;
369
+ const safeCount = Math.min(3, Math.max(1, Math.round(metricCount)));
370
+ const width = FLOATING_WIDGET_WIDTHS[safeCount - 1];
371
+ const [currentWidth] = window.getSize();
372
+ if (currentWidth === width)
373
+ return;
374
+ const [x, y] = window.getPosition();
375
+ window.setSize(width, FLOATING_WIDGET_HEIGHT, false);
376
+ const constrained = constrainFloatingPosition({ x: x + currentWidth - width, y }, width);
377
+ window.setPosition(constrained.x, constrained.y, false);
378
+ }
288
379
  function refreshFloatingWidget(status) {
289
380
  if (process.platform !== 'darwin' || !status.preferences.floatingWidgetEnabled) {
290
381
  if (floatingWidgetWindow && !floatingWidgetWindow.isDestroyed())
@@ -300,7 +391,8 @@ function keepFloatingWidgetOnScreen() {
300
391
  if (!floatingWidgetWindow || floatingWidgetWindow.isDestroyed())
301
392
  return;
302
393
  const [x, y] = floatingWidgetWindow.getPosition();
303
- const constrained = constrainFloatingPosition({ x, y });
394
+ const [width] = floatingWidgetWindow.getSize();
395
+ const constrained = constrainFloatingPosition({ x, y }, width);
304
396
  if (constrained.x !== x || constrained.y !== y) {
305
397
  floatingWidgetWindow.setPosition(constrained.x, constrained.y, false);
306
398
  }
@@ -379,6 +471,7 @@ function defaultConfig() {
379
471
  version: 1,
380
472
  connections: [],
381
473
  lastConnectionUrl: null,
474
+ openConnectionUrls: [],
382
475
  trustedCertificateAuthorities: [],
383
476
  desktopPreferences: {
384
477
  menuBarStatusEnabled: true,
@@ -517,13 +610,20 @@ function readDesktopConfig() {
517
610
  const parsed = JSON.parse(fs.readFileSync(desktopConfigPath, 'utf8'));
518
611
  if (parsed.version !== 1 || !Array.isArray(parsed.connections))
519
612
  return defaultConfig();
613
+ const lastConnectionUrl = typeof parsed.lastConnectionUrl === 'string' ? parsed.lastConnectionUrl : null;
520
614
  return {
521
615
  version: 1,
522
616
  connections: parsed.connections.filter((entry) => Boolean(entry)
523
617
  && typeof entry.id === 'string'
524
618
  && typeof entry.label === 'string'
525
619
  && typeof entry.url === 'string'),
526
- lastConnectionUrl: typeof parsed.lastConnectionUrl === 'string' ? parsed.lastConnectionUrl : null,
620
+ lastConnectionUrl,
621
+ // Older desktop.json files only remembered the most recent window. Use
622
+ // that value once as their restore list, while preserving an explicit
623
+ // empty list when the user intentionally closed every service window.
624
+ openConnectionUrls: Array.isArray(parsed.openConnectionUrls)
625
+ ? parsed.openConnectionUrls.filter((url) => typeof url === 'string')
626
+ : lastConnectionUrl ? [lastConnectionUrl] : [],
527
627
  trustedCertificateAuthorities: Array.isArray(parsed.trustedCertificateAuthorities)
528
628
  ? parsed.trustedCertificateAuthorities.filter((entry) => Boolean(entry)
529
629
  && typeof entry.origin === 'string'
@@ -1057,7 +1157,7 @@ async function installCli() {
1057
1157
  ], { timeout: 120_000, maxBuffer: 256 * 1024 });
1058
1158
  return snapshot();
1059
1159
  }
1060
- async function connectWindow(rawUrl) {
1160
+ async function connectWindow(rawUrl, options = {}) {
1061
1161
  let probe = await probeServiceWithLocalNetworkPermission(rawUrl);
1062
1162
  if (!probe.ok)
1063
1163
  return probe;
@@ -1066,13 +1166,30 @@ async function connectWindow(rawUrl) {
1066
1166
  const existingWindow = serviceWindows.get(key);
1067
1167
  if (existingWindow && !existingWindow.isDestroyed()) {
1068
1168
  const config = readDesktopConfig();
1069
- config.lastConnectionUrl = probe.url;
1169
+ if (options.updateLastConnection !== false)
1170
+ config.lastConnectionUrl = probe.url;
1171
+ config.openConnectionUrls = [
1172
+ ...config.openConnectionUrls.filter((url) => {
1173
+ try {
1174
+ return new URL(url).origin !== key;
1175
+ }
1176
+ catch {
1177
+ return false;
1178
+ }
1179
+ }),
1180
+ probe.url,
1181
+ ];
1070
1182
  const existing = config.connections.find((entry) => entry.url === probe.url);
1071
1183
  if (existing)
1072
1184
  existing.lastConnectedAt = Date.now();
1073
1185
  writeDesktopConfig(config);
1074
- mainWindow?.hide();
1075
- showAndFocusWindow(existingWindow);
1186
+ if (options.focus !== false) {
1187
+ mainWindow?.hide();
1188
+ showAndFocusWindow(existingWindow);
1189
+ }
1190
+ else {
1191
+ existingWindow.showInactive();
1192
+ }
1076
1193
  return probe;
1077
1194
  }
1078
1195
  const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
@@ -1082,13 +1199,34 @@ async function connectWindow(rawUrl) {
1082
1199
  try {
1083
1200
  await workspaceWindow.loadURL(probe.url);
1084
1201
  const config = readDesktopConfig();
1085
- config.lastConnectionUrl = probe.url;
1202
+ if (options.updateLastConnection !== false)
1203
+ config.lastConnectionUrl = probe.url;
1204
+ config.openConnectionUrls = [
1205
+ ...config.openConnectionUrls.filter((url) => {
1206
+ try {
1207
+ return new URL(url).origin !== key;
1208
+ }
1209
+ catch {
1210
+ return false;
1211
+ }
1212
+ }),
1213
+ probe.url,
1214
+ ];
1086
1215
  const existing = config.connections.find((entry) => entry.url === probe.url);
1087
1216
  if (existing)
1088
1217
  existing.lastConnectedAt = Date.now();
1089
1218
  writeDesktopConfig(config);
1090
- mainWindow?.hide();
1091
- showAndFocusWindow(workspaceWindow);
1219
+ restorableServiceWindows.add(workspaceWindow);
1220
+ if (options.focus !== false) {
1221
+ mainWindow?.hide();
1222
+ showAndFocusWindow(workspaceWindow);
1223
+ }
1224
+ else {
1225
+ // Restored workspaces must be real visible macOS windows so App Expose
1226
+ // can select them, but opening several at launch must not steal focus
1227
+ // from each other.
1228
+ workspaceWindow.showInactive();
1229
+ }
1092
1230
  return probe;
1093
1231
  }
1094
1232
  catch (error) {
@@ -1291,6 +1429,19 @@ function createDesktopWindow(options) {
1291
1429
  clearUnreadNotifications();
1292
1430
  });
1293
1431
  window.on('close', (event) => {
1432
+ const serviceOrigin = windowServiceOrigins.get(window);
1433
+ if (serviceOrigin && !isQuitting && restorableServiceWindows.has(window)) {
1434
+ const config = readDesktopConfig();
1435
+ config.openConnectionUrls = config.openConnectionUrls.filter((url) => {
1436
+ try {
1437
+ return new URL(url).origin !== serviceOrigin;
1438
+ }
1439
+ catch {
1440
+ return false;
1441
+ }
1442
+ });
1443
+ writeDesktopConfig(config);
1444
+ }
1294
1445
  if (window === mainWindow && process.platform === 'darwin' && !isQuitting) {
1295
1446
  event.preventDefault();
1296
1447
  window.hide();
@@ -1390,6 +1541,12 @@ function installIpcHandlers() {
1390
1541
  : 'attention';
1391
1542
  focusNextService(normalized);
1392
1543
  });
1544
+ ipcMain.handle('desktop:set-floating-metric-count', (event, count) => {
1545
+ const sourceWindow = BrowserWindow.fromWebContents(event.sender);
1546
+ if (sourceWindow !== floatingWidgetWindow || typeof count !== 'number' || !Number.isFinite(count))
1547
+ return;
1548
+ resizeFloatingWidget(count);
1549
+ });
1393
1550
  ipcMain.handle('desktop:disable-floating-widget', async () => {
1394
1551
  updateDesktopPreferences({ floatingWidgetEnabled: false });
1395
1552
  return snapshot();
@@ -1681,11 +1838,27 @@ app.whenReady().then(async () => {
1681
1838
  window.webContents.send('desktop:update-state-changed', state);
1682
1839
  }
1683
1840
  });
1684
- const lastConnectionUrl = readDesktopConfig().lastConnectionUrl;
1685
- if (lastConnectionUrl) {
1686
- const probe = await connectWindow(lastConnectionUrl);
1687
- if (!probe.ok)
1688
- await showConnectionCenter();
1841
+ const startupConfig = readDesktopConfig();
1842
+ const restoreUrls = startupConfig.openConnectionUrls;
1843
+ let restoredWindowCount = 0;
1844
+ for (const url of restoreUrls) {
1845
+ const probe = await connectWindow(url, { focus: false, updateLastConnection: false });
1846
+ if (probe.ok)
1847
+ restoredWindowCount += 1;
1848
+ }
1849
+ if (restoredWindowCount > 0) {
1850
+ let preferredOrigin = null;
1851
+ try {
1852
+ preferredOrigin = startupConfig.lastConnectionUrl
1853
+ ? new URL(startupConfig.lastConnectionUrl).origin
1854
+ : null;
1855
+ }
1856
+ catch {
1857
+ // A malformed legacy value must not prevent the other saved windows
1858
+ // from being restored.
1859
+ }
1860
+ const preferredWindow = preferredOrigin ? serviceWindows.get(preferredOrigin) : null;
1861
+ showAndFocusWindow(preferredWindow ?? [...serviceWindows.values()][0]);
1689
1862
  }
1690
1863
  else {
1691
1864
  await showConnectionCenter();