termdock 1.4.124 → 1.4.125

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 (69) hide show
  1. package/.desktop-dist/main.js +151 -43
  2. package/dist/client/assets/{ModelPreview-Be2smem8.js → ModelPreview-ChnYwKkY.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-BlflMbiO.js → abnfDiagram-VRR7QNED-rm8HZhdr.js} +1 -1
  4. package/dist/client/assets/{arc-BLcH6XEC.js → arc-lmX0JkP8.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CnBbD-1o.js → architectureDiagram-ZJ3FMSHR-B-fNsEWy.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-DPF-Ts3E.js → blockDiagram-677ZJIJ3-CcKyp1yy.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-bj_qgr87.js → c4Diagram-LMCZKHZV-BceB7WNk.js} +1 -1
  8. package/dist/client/assets/channel-BgyemVuE.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-BLPLyR9K.js → chunk-2Q5K7J3B-CKl1IhTX.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-DWlXbXJt.js → chunk-32BRIVSS-Gu2AH7Vq.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-CEM0N0ye.js → chunk-5VM5RSS4-DdzVDT3j.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-Dty9y4gw.js → chunk-EX3LRPZG-CiOx9i27.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-dlwhK_i1.js → chunk-JWPE2WC7-Dm5NkQb2.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-CX08FiG7.js → chunk-MOJQB5TN-BlzxmFLN.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-CoYfVCEL.js → chunk-RYQCIY6F-CwC-3LxS.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-C_nJAEZ6.js → chunk-V7JOEXUC-BPqNLPST.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-CxTZ4h0g.js → chunk-VR4S4FIN-DcBWXics.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-CRcElLUQ.js → chunk-XXDRQBXY-BoqBy-W-.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-RYUGciz-.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-RYUGciz-.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-DopCQZMr.js → cose-bilkent-JH36ORCC-DNQ-dGzx.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-C5lwo5rG.js → cynefin-VYW2F7L2-CiP8cZ32.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-BqE-HnNf.js → cynefinDiagram-TSTJHNR4-DOBKA-xo.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-Qnuvs3u5.js → dagre-VKFMJZFB-hvy3FbtL.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-T6rXZ6sy.js → diagram-FQU43EPY-CNwULa6G.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-BlKH1yFL.js → diagram-G47NLZAW-DHADT4lq.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-BsAh_h83.js → diagram-NH7WQ7WH-BmmJLM_2.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-DFFBJ56F.js → diagram-OA4YK3LP-6UeJa1HZ.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-BAt9TFgr.js → diagram-WEI45ONY-Di2QIkMn.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-CZqTgzyZ.js → ebnfDiagram-CCIWWBDH-Clm1K5fA.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-Des9G2E2.js → erDiagram-Q63AITRT-Dp2qDi_n.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-D-GCzapP.js → flowDiagram-23GEKE2U-C46rLAR2.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-BGH_iM7_.js → ganttDiagram-NO4QXBWP-DOiDcHms.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-dheKFUpw.js → gitGraphDiagram-IHSO6WYX-BOrXbgha.js} +1 -1
  35. package/dist/client/assets/{index-DjYnI5kP.js → index-COymhGhN.js} +71 -71
  36. package/dist/client/assets/{infoDiagram-FWYZ7A6U-BZYW2clU.js → infoDiagram-FWYZ7A6U-CdkS3Dfg.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-Di9GvVpK.js → ishikawaDiagram-FXEZZL3T-rqxY2FYN.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-5HDEW3XC-Bm_oN9-6.js → journeyDiagram-5HDEW3XC-C9qqTWX3.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-HUTT4EX6-tj1z1JQG.js → kanban-definition-HUTT4EX6-DmiQ6p00.js} +1 -1
  40. package/dist/client/assets/{linear-QXJ-Ae7F.js → linear-CV4QbX1j.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-C2fDWTc4.js → mermaid.core-ioXNdjpL.js} +4 -4
  42. package/dist/client/assets/{mindmap-definition-LN4V7U3C-BilCFNik.js → mindmap-definition-LN4V7U3C-qmk3HlIh.js} +1 -1
  43. package/dist/client/assets/{pegDiagram-2B236MQR-Cedi8jLg.js → pegDiagram-2B236MQR-B5x--HGM.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-ENE6RG2P-u8doKeCT.js → pieDiagram-ENE6RG2P-BiPzRE89.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CTksDMFb.js → quadrantDiagram-ABIIQ3AL-DWSljFGl.js} +1 -1
  46. package/dist/client/assets/{railroadDiagram-RFXS5EU6-a3hmKcli.js → railroadDiagram-RFXS5EU6-DC5NPyZJ.js} +1 -1
  47. package/dist/client/assets/{requirementDiagram-TGXJPOKE-0CbosJM9.js → requirementDiagram-TGXJPOKE-DQRJohSF.js} +1 -1
  48. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-DM9MM942.js → sankeyDiagram-HTMAVEWB-DSA0rGbL.js} +1 -1
  49. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-DJYOI_VF.js → sequenceDiagram-DBY2YBRQ-C5P5a5rg.js} +1 -1
  50. package/dist/client/assets/{sizeCapture-X5ZJPWSS-CayWxIRi.js → sizeCapture-X5ZJPWSS-C0-OJWn8.js} +1 -1
  51. package/dist/client/assets/{stateDiagram-2N3HPSRC-BktX6ilb.js → stateDiagram-2N3HPSRC-Biektd4J.js} +1 -1
  52. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CqyLBZuz.js +1 -0
  53. package/dist/client/assets/{swimlanes-5IMT3BWC-Duvbluwj.js → swimlanes-5IMT3BWC-CeZS6uBf.js} +2 -2
  54. package/dist/client/assets/swimlanesDiagram-G3AALYLV-Cl_Lmma0.js +8 -0
  55. package/dist/client/assets/{timeline-definition-FHXFAJF6-TdPH8RG8.js → timeline-definition-FHXFAJF6-DbcHM5pN.js} +1 -1
  56. package/dist/client/assets/{vennDiagram-L72KCM5P-MQSsBdGx.js → vennDiagram-L72KCM5P-Dhv6WXnx.js} +1 -1
  57. package/dist/client/assets/{wardleyDiagram-EHGQE667-BcsapLTw.js → wardleyDiagram-EHGQE667-8ef6C53J.js} +1 -1
  58. package/dist/client/assets/{xychartDiagram-FW5EYKEG-BSUPwtFh.js → xychartDiagram-FW5EYKEG-DihvzNhK.js} +1 -1
  59. package/dist/client/index.html +1 -1
  60. package/dist/client/sw.js +1 -1
  61. package/dist/server/entry.js +1 -0
  62. package/dist/server/routes/terminal.js +12 -0
  63. package/package.json +1 -1
  64. package/runtime-manifest.json +2 -2
  65. package/dist/client/assets/channel-CwqZRIbt.js +0 -1
  66. package/dist/client/assets/classDiagram-OUVF2IWQ-CsBWdSSp.js +0 -1
  67. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CsBWdSSp.js +0 -1
  68. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-DK505RoL.js +0 -1
  69. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CZTzB1q3.js +0 -8
@@ -9,7 +9,7 @@ import path from 'node:path';
9
9
  import { promisify } from 'node:util';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { desktopStatusTooltip, formatCompactDesktopStatus, menuBarStatusRows, mergeServiceActivity, nextServiceOrigin, normalizeServiceActivity, summarizeServiceActivity, } from './activityStatus.js';
12
- import { checkForDesktopUpdates, configureDesktopUpdater, ensureLatestRuntime, getDesktopUpdateState, installDownloadedDesktopUpdate, subscribeDesktopUpdateState, } from './updater.js';
12
+ import { checkForDesktopUpdates, checkForRuntimeUpdates, configureDesktopUpdater, ensureLatestRuntime, getDesktopUpdateState, getDesktopRuntimeUpdateState, installDownloadedDesktopUpdate, markDesktopRuntimeRestarting, markDesktopRuntimeRunning, subscribeDesktopRuntimeUpdateState, subscribeDesktopUpdateState, } from './updater.js';
13
13
  import { resolvePackagedRuntime, rollbackDownloadedRuntime, } from './runtime.js';
14
14
  import { isExternalLinkStagingUrl, isSafeExternalUrl } from './externalLinks.js';
15
15
  import { shouldThrottleDesktopRenderer } from './windowPolicy.js';
@@ -24,6 +24,7 @@ const DEFAULT_LOCAL_URL = 'http://localhost:9834';
24
24
  const PROTOCOL_VERSION = 1;
25
25
  const HEALTH_TIMEOUT_MS = 3_500;
26
26
  const START_TIMEOUT_MS = 90_000;
27
+ const RESTORE_LOAD_TIMEOUT_MS = 15_000;
27
28
  const localServiceCertificatePath = path.join(termdockDir, 'certs', 'termdock-local.pem');
28
29
  const sessionTrustedCertificateTargets = new Set();
29
30
  const sessionTrustedCertificateAuthorities = new Map();
@@ -51,6 +52,8 @@ function triggerLocalNetworkPermission() {
51
52
  socket.send(Buffer.from('Termdock'), 5353, '224.0.0.251', close);
52
53
  }
53
54
  let mainWindow = null;
55
+ let startupRestoreActive = false;
56
+ let startupProgressMessage = '正在启动 Termdock Desktop…';
54
57
  const serviceWindows = new Map();
55
58
  const windowServiceOrigins = new WeakMap();
56
59
  const restorableServiceWindows = new WeakSet();
@@ -752,6 +755,7 @@ async function probeServiceWithCertificateAuthority(rawUrl, certificateAuthority
752
755
  protocolVersion: typeof metadata?.protocolVersion === 'number'
753
756
  ? metadata.protocolVersion
754
757
  : undefined,
758
+ desktopManaged: metadata?.desktopManaged === true,
755
759
  };
756
760
  }
757
761
  }
@@ -795,6 +799,7 @@ async function probeService(rawUrl) {
795
799
  protocolVersion: typeof metadata.protocolVersion === 'number'
796
800
  ? metadata.protocolVersion
797
801
  : undefined,
802
+ desktopManaged: metadata.desktopManaged === true,
798
803
  };
799
804
  }
800
805
  }
@@ -811,7 +816,7 @@ async function probeService(rawUrl) {
811
816
  return { ok: false, url, error: message };
812
817
  }
813
818
  }
814
- async function probeServiceWithLocalNetworkPermission(rawUrl) {
819
+ async function probeServiceWithLocalNetworkPermission(rawUrl, options = {}) {
815
820
  let normalizedUrl;
816
821
  try {
817
822
  normalizedUrl = normalizeServiceUrl(rawUrl);
@@ -838,6 +843,8 @@ async function probeServiceWithLocalNetworkPermission(rawUrl) {
838
843
  return probe;
839
844
  }
840
845
  if (canOfferCertificateTrust(probe.url) && isCertificateTrustError(probe.error)) {
846
+ if (options.interactive === false)
847
+ return probe;
841
848
  const certificate = await requestCertificateTrust(probe.url);
842
849
  if (!certificate) {
843
850
  return { ...probe, error: 'HTTPS 证书尚未受信任' };
@@ -847,6 +854,8 @@ async function probeServiceWithLocalNetworkPermission(rawUrl) {
847
854
  if (!isLocalNetworkTarget(parsedProbeUrl) || !looksLikeLocalNetworkPermissionError(probe.error)) {
848
855
  return probe;
849
856
  }
857
+ if (options.interactive === false)
858
+ return probe;
850
859
  if (!(await requestLocalNetworkPermissionRetry(probe.url))) {
851
860
  return { ...probe, error: 'Termdock 尚未获得 macOS 本地网络权限' };
852
861
  }
@@ -967,11 +976,16 @@ async function snapshot() {
967
976
  }
968
977
  function desktopRuntimeEnv(runtime = runtimePaths()) {
969
978
  const currentPath = process.env.PATH ?? '/usr/bin:/bin:/usr/sbin:/sbin';
979
+ const userBinPaths = [
980
+ path.join(os.homedir(), '.local', 'bin'),
981
+ path.join(os.homedir(), 'bin'),
982
+ path.join(os.homedir(), '.npm-global', 'bin'),
983
+ ];
970
984
  return {
971
985
  ...process.env,
972
986
  LANG: process.env.LANG || 'en_US.UTF-8',
973
987
  LC_CTYPE: process.env.LC_CTYPE || process.env.LANG || 'en_US.UTF-8',
974
- PATH: [runtime.toolchainBin, '/opt/homebrew/bin', '/usr/local/bin', currentPath]
988
+ PATH: [runtime.toolchainBin, ...userBinPaths, '/opt/homebrew/bin', '/usr/local/bin', currentPath]
975
989
  .filter(Boolean)
976
990
  .join(path.delimiter),
977
991
  TERMDOCK_DESKTOP: '1',
@@ -1114,9 +1128,35 @@ async function startLocalService() {
1114
1128
  throw new Error(`桌面版服务启动失败:${probe.error ?? '未知错误'}。日志:${path.join(termdockDir, 'server.log')}`);
1115
1129
  }
1116
1130
  await ensureTmuxUtf8Environment(runtime);
1131
+ markDesktopRuntimeRunning(runtime.version);
1117
1132
  await connectWindow(probe.url);
1118
1133
  return probe;
1119
1134
  }
1135
+ async function restartDesktopManagedRuntime() {
1136
+ const status = await getLocalServiceStatus();
1137
+ if (status.running) {
1138
+ if (status.probe?.desktopManaged !== true) {
1139
+ throw new Error('当前本机服务不是由桌面版启动的,不能由桌面版自动重启。');
1140
+ }
1141
+ markDesktopRuntimeRestarting();
1142
+ const pid = status.state?.pid;
1143
+ if (!pid || !isProcessRunning(pid)) {
1144
+ throw new Error('桌面服务状态已失效,请重新打开连接中心。');
1145
+ }
1146
+ process.kill(pid, 'SIGTERM');
1147
+ const deadline = Date.now() + 15_000;
1148
+ while (isProcessRunning(pid) && Date.now() < deadline) {
1149
+ await new Promise((resolve) => setTimeout(resolve, 150));
1150
+ }
1151
+ if (isProcessRunning(pid))
1152
+ throw new Error(`桌面服务 PID ${pid} 未能正常退出。`);
1153
+ const latest = readServerState();
1154
+ if (latest?.pid === pid)
1155
+ fs.rmSync(serverStatePath, { force: true });
1156
+ }
1157
+ await startLocalService();
1158
+ return getDesktopRuntimeUpdateState();
1159
+ }
1120
1160
  function shellQuote(value) {
1121
1161
  return `'${value.replace(/'/g, `'\\''`)}'`;
1122
1162
  }
@@ -1158,46 +1198,16 @@ async function installCli() {
1158
1198
  return snapshot();
1159
1199
  }
1160
1200
  async function connectWindow(rawUrl, options = {}) {
1161
- let probe = await probeServiceWithLocalNetworkPermission(rawUrl);
1201
+ let probe = await probeServiceWithLocalNetworkPermission(rawUrl, {
1202
+ interactive: options.focus !== false,
1203
+ });
1162
1204
  if (!probe.ok)
1163
1205
  return probe;
1164
1206
  const parsed = new URL(probe.url);
1165
1207
  const key = parsed.origin;
1166
1208
  const existingWindow = serviceWindows.get(key);
1167
1209
  if (existingWindow && !existingWindow.isDestroyed()) {
1168
- const config = readDesktopConfig();
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
- ];
1182
- const existing = config.connections.find((entry) => entry.url === probe.url);
1183
- if (existing)
1184
- existing.lastConnectedAt = Date.now();
1185
- writeDesktopConfig(config);
1186
- if (options.focus !== false) {
1187
- mainWindow?.hide();
1188
- showAndFocusWindow(existingWindow);
1189
- }
1190
- else {
1191
- existingWindow.showInactive();
1192
- }
1193
- return probe;
1194
- }
1195
- const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
1196
- serviceWindows.set(key, workspaceWindow);
1197
- broadcastServiceActivity();
1198
- while (!workspaceWindow.isDestroyed()) {
1199
- try {
1200
- await workspaceWindow.loadURL(probe.url);
1210
+ if (options.persist !== false) {
1201
1211
  const config = readDesktopConfig();
1202
1212
  if (options.updateLastConnection !== false)
1203
1213
  config.lastConnectionUrl = probe.url;
@@ -1216,6 +1226,54 @@ async function connectWindow(rawUrl, options = {}) {
1216
1226
  if (existing)
1217
1227
  existing.lastConnectedAt = Date.now();
1218
1228
  writeDesktopConfig(config);
1229
+ }
1230
+ if (options.focus !== false) {
1231
+ mainWindow?.hide();
1232
+ showAndFocusWindow(existingWindow);
1233
+ }
1234
+ else {
1235
+ existingWindow.showInactive();
1236
+ }
1237
+ return probe;
1238
+ }
1239
+ const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
1240
+ serviceWindows.set(key, workspaceWindow);
1241
+ broadcastServiceActivity();
1242
+ while (!workspaceWindow.isDestroyed()) {
1243
+ try {
1244
+ const load = workspaceWindow.loadURL(probe.url);
1245
+ if (options.focus === false) {
1246
+ await Promise.race([
1247
+ load,
1248
+ new Promise((_resolve, reject) => {
1249
+ const timer = setTimeout(() => reject(new Error('恢复连接超时')), RESTORE_LOAD_TIMEOUT_MS);
1250
+ timer.unref();
1251
+ }),
1252
+ ]);
1253
+ }
1254
+ else {
1255
+ await load;
1256
+ }
1257
+ if (options.persist !== false) {
1258
+ const config = readDesktopConfig();
1259
+ if (options.updateLastConnection !== false)
1260
+ config.lastConnectionUrl = probe.url;
1261
+ config.openConnectionUrls = [
1262
+ ...config.openConnectionUrls.filter((url) => {
1263
+ try {
1264
+ return new URL(url).origin !== key;
1265
+ }
1266
+ catch {
1267
+ return false;
1268
+ }
1269
+ }),
1270
+ probe.url,
1271
+ ];
1272
+ const existing = config.connections.find((entry) => entry.url === probe.url);
1273
+ if (existing)
1274
+ existing.lastConnectedAt = Date.now();
1275
+ writeDesktopConfig(config);
1276
+ }
1219
1277
  restorableServiceWindows.add(workspaceWindow);
1220
1278
  if (options.focus !== false) {
1221
1279
  mainWindow?.hide();
@@ -1231,6 +1289,10 @@ async function connectWindow(rawUrl, options = {}) {
1231
1289
  }
1232
1290
  catch (error) {
1233
1291
  const message = networkErrorDetails(error);
1292
+ if (options.focus === false) {
1293
+ workspaceWindow.destroy();
1294
+ return { ...probe, ok: false, error: message };
1295
+ }
1234
1296
  if (isLocalNetworkTarget(parsed) && looksLikeLocalNetworkPermissionError(error)) {
1235
1297
  if (await requestLocalNetworkPermissionRetry(probe.url))
1236
1298
  continue;
@@ -1260,6 +1322,29 @@ async function showConnectionCenter() {
1260
1322
  if (mainWindow)
1261
1323
  showAndFocusWindow(mainWindow);
1262
1324
  }
1325
+ async function showStartupProgress(message) {
1326
+ if (!startupRestoreActive)
1327
+ return;
1328
+ startupProgressMessage = message;
1329
+ await showConnectionCenter();
1330
+ if (!startupRestoreActive || !mainWindow || mainWindow.isDestroyed())
1331
+ return;
1332
+ mainWindow.webContents.send('desktop:startup-progress', startupProgressMessage);
1333
+ }
1334
+ function updateStartupProgress(message) {
1335
+ if (!startupRestoreActive)
1336
+ return;
1337
+ startupProgressMessage = message;
1338
+ if (mainWindow && !mainWindow.isDestroyed()) {
1339
+ mainWindow.webContents.send('desktop:startup-progress', startupProgressMessage);
1340
+ }
1341
+ }
1342
+ function finishStartupProgress() {
1343
+ startupRestoreActive = false;
1344
+ if (!mainWindow || mainWindow.isDestroyed())
1345
+ return;
1346
+ mainWindow.webContents.send('desktop:startup-progress', null);
1347
+ }
1263
1348
  function createDesktopWindow(options) {
1264
1349
  const serviceSession = options
1265
1350
  ? session.fromPartition(`persist:termdock-service-${crypto
@@ -1555,6 +1640,9 @@ function installIpcHandlers() {
1555
1640
  ipcMain.handle('desktop:update-state', () => getDesktopUpdateState());
1556
1641
  ipcMain.handle('desktop:check-update', () => checkForDesktopUpdates());
1557
1642
  ipcMain.handle('desktop:install-update', () => installDownloadedDesktopUpdate());
1643
+ ipcMain.handle('desktop:runtime-update-state', () => getDesktopRuntimeUpdateState());
1644
+ ipcMain.handle('desktop:check-runtime-update', () => checkForRuntimeUpdates());
1645
+ ipcMain.handle('desktop:restart-runtime', () => restartDesktopManagedRuntime());
1558
1646
  ipcMain.handle('desktop:show-connection-center', () => showConnectionCenter());
1559
1647
  ipcMain.handle('desktop:reveal-data-directory', async () => {
1560
1648
  fs.mkdirSync(termdockDir, { recursive: true, mode: 0o700 });
@@ -1838,14 +1926,34 @@ app.whenReady().then(async () => {
1838
1926
  window.webContents.send('desktop:update-state-changed', state);
1839
1927
  }
1840
1928
  });
1929
+ subscribeDesktopRuntimeUpdateState((state) => {
1930
+ for (const window of BrowserWindow.getAllWindows()) {
1931
+ if (!window.isDestroyed())
1932
+ window.webContents.send('desktop:runtime-update-state-changed', state);
1933
+ }
1934
+ });
1841
1935
  const startupConfig = readDesktopConfig();
1842
1936
  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
- }
1937
+ startupRestoreActive = true;
1938
+ const startupProgressTimer = setTimeout(() => {
1939
+ void showStartupProgress('正在检查本机 Runtime 和已保存连接…');
1940
+ }, 350);
1941
+ startupProgressTimer.unref();
1942
+ let completedRestores = 0;
1943
+ updateStartupProgress(`正在恢复连接 0/${restoreUrls.length}…`);
1944
+ const restoreResults = await Promise.all(restoreUrls.map(async (url) => {
1945
+ const probe = await connectWindow(url, {
1946
+ focus: false,
1947
+ updateLastConnection: false,
1948
+ persist: false,
1949
+ });
1950
+ completedRestores += 1;
1951
+ updateStartupProgress(`正在恢复连接 ${completedRestores}/${restoreUrls.length}…`);
1952
+ return probe;
1953
+ }));
1954
+ const restoredWindowCount = restoreResults.filter((probe) => probe.ok).length;
1955
+ clearTimeout(startupProgressTimer);
1956
+ finishStartupProgress();
1849
1957
  if (restoredWindowCount > 0) {
1850
1958
  let preferredOrigin = null;
1851
1959
  try {