termdock 1.4.69 → 1.4.71

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 (84) hide show
  1. package/.desktop-dist/main.js +254 -49
  2. package/README.md +6 -0
  3. package/dist/client/assets/{ModelPreview-Bk8GM7G-.js → ModelPreview-CG6vLwyw.js} +1 -1
  4. package/dist/client/assets/{abnfDiagram-VRR7QNED-BYiQXGYk.js → abnfDiagram-VRR7QNED-CrXEZLWX.js} +1 -1
  5. package/dist/client/assets/{arc-jlT_jmoZ.js → arc-DbKGR7Ex.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D1qZ875Q.js → architectureDiagram-ZJ3FMSHR-PF5BoUbI.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-677ZJIJ3-qkgn2Ysc.js → blockDiagram-677ZJIJ3-D1gRzVWB.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-LMCZKHZV-JgcH49Pf.js → c4Diagram-LMCZKHZV-CwvOLync.js} +1 -1
  9. package/dist/client/assets/channel-JTEuKVy6.js +1 -0
  10. package/dist/client/assets/{chunk-2Q5K7J3B-E-3iflEy.js → chunk-2Q5K7J3B-BeRdP-xP.js} +1 -1
  11. package/dist/client/assets/{chunk-32BRIVSS-mM01y3kF.js → chunk-32BRIVSS-CrqR5rvl.js} +1 -1
  12. package/dist/client/assets/{chunk-5VM5RSS4-GO8Muo5l.js → chunk-5VM5RSS4-CjbUr-ko.js} +1 -1
  13. package/dist/client/assets/{chunk-EX3LRPZG-BUSDluH0.js → chunk-EX3LRPZG-BVEk3Wma.js} +1 -1
  14. package/dist/client/assets/{chunk-JWPE2WC7-obSL6EzJ.js → chunk-JWPE2WC7-WitJe8kR.js} +1 -1
  15. package/dist/client/assets/{chunk-MOJQB5TN-zEktbtAG.js → chunk-MOJQB5TN-BCewlLYk.js} +1 -1
  16. package/dist/client/assets/{chunk-RYQCIY6F-kH7H8XgQ.js → chunk-RYQCIY6F-CoP0AA4G.js} +1 -1
  17. package/dist/client/assets/{chunk-V7JOEXUC-BqDFpHKN.js → chunk-V7JOEXUC-CSZrnmp_.js} +1 -1
  18. package/dist/client/assets/{chunk-VR4S4FIN-BGZ7kCIG.js → chunk-VR4S4FIN-BB-7zz-9.js} +1 -1
  19. package/dist/client/assets/{chunk-XXDRQBXY-D-TatWXe.js → chunk-XXDRQBXY-CcZZlCik.js} +1 -1
  20. package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +1 -0
  22. package/dist/client/assets/{cose-bilkent-JH36ORCC-CfWyrViz.js → cose-bilkent-JH36ORCC-DcmzUDwh.js} +1 -1
  23. package/dist/client/assets/{cynefin-VYW2F7L2-BFdZ3FcE.js → cynefin-VYW2F7L2-C1XlDVoJ.js} +1 -1
  24. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-iAfCVoFU.js → cynefinDiagram-TSTJHNR4-zY6Aet2S.js} +1 -1
  25. package/dist/client/assets/{dagre-VKFMJZFB-CZhh0hrr.js → dagre-VKFMJZFB-DqGTi6zR.js} +1 -1
  26. package/dist/client/assets/{diagram-FQU43EPY-BqUK2OmV.js → diagram-FQU43EPY-DStcsU7S.js} +1 -1
  27. package/dist/client/assets/{diagram-G47NLZAW-vC0pSQYC.js → diagram-G47NLZAW-Cs6IMNip.js} +1 -1
  28. package/dist/client/assets/{diagram-NH7WQ7WH-Dqc5oxed.js → diagram-NH7WQ7WH-yA2bpWN7.js} +1 -1
  29. package/dist/client/assets/{diagram-OA4YK3LP-DIIXckFE.js → diagram-OA4YK3LP-Dzf9M0PI.js} +1 -1
  30. package/dist/client/assets/{diagram-WEI45ONY-DR9_cFeh.js → diagram-WEI45ONY-D75lpQKA.js} +1 -1
  31. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-21OO0OXC.js → ebnfDiagram-CCIWWBDH-CkQvIpWy.js} +1 -1
  32. package/dist/client/assets/{erDiagram-Q63AITRT-YKnVG3az.js → erDiagram-Q63AITRT-Ca00Vjxz.js} +1 -1
  33. package/dist/client/assets/{flowDiagram-23GEKE2U-Cm7TGErF.js → flowDiagram-23GEKE2U-CGrnSTEp.js} +1 -1
  34. package/dist/client/assets/{ganttDiagram-NO4QXBWP-D_76Effz.js → ganttDiagram-NO4QXBWP-0SHfOIP4.js} +1 -1
  35. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CC1ysmVD.js → gitGraphDiagram-IHSO6WYX-BeigYWvs.js} +1 -1
  36. package/dist/client/assets/index-D5PKdbhW.js +451 -0
  37. package/dist/client/assets/index-DhfIiBLW.css +1 -0
  38. package/dist/client/assets/{infoDiagram-FWYZ7A6U-BbgpmhBz.js → infoDiagram-FWYZ7A6U-CiY1-xjN.js} +1 -1
  39. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C94bY_je.js → ishikawaDiagram-FXEZZL3T-CmtDjUm_.js} +1 -1
  40. package/dist/client/assets/{journeyDiagram-5HDEW3XC-BQ0iFRxs.js → journeyDiagram-5HDEW3XC-B7qOr9LY.js} +1 -1
  41. package/dist/client/assets/{kanban-definition-HUTT4EX6-BtAexXPo.js → kanban-definition-HUTT4EX6-DO2P2Um3.js} +1 -1
  42. package/dist/client/assets/{linear-C2HnIdOK.js → linear-B832hJt0.js} +1 -1
  43. package/dist/client/assets/{mermaid.core-BbG4sJBF.js → mermaid.core-CsAzdiCU.js} +4 -4
  44. package/dist/client/assets/{mindmap-definition-LN4V7U3C-B-uvViws.js → mindmap-definition-LN4V7U3C-HI__vXhQ.js} +1 -1
  45. package/dist/client/assets/{pegDiagram-2B236MQR-DHYcR6tE.js → pegDiagram-2B236MQR--rZIwjXq.js} +1 -1
  46. package/dist/client/assets/{pieDiagram-ENE6RG2P-DEE9cD6M.js → pieDiagram-ENE6RG2P-D9jPf5x9.js} +1 -1
  47. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DMQP5jG3.js → quadrantDiagram-ABIIQ3AL-DTCP5ji2.js} +1 -1
  48. package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-4WDTwc.js → railroadDiagram-RFXS5EU6-D2Gspvrl.js} +1 -1
  49. package/dist/client/assets/{requirementDiagram-TGXJPOKE-C2xw_q-R.js → requirementDiagram-TGXJPOKE-DEJC02IW.js} +1 -1
  50. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-wq0BPqhc.js → sankeyDiagram-HTMAVEWB-opOhKPap.js} +1 -1
  51. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BqpM0A9d.js → sequenceDiagram-DBY2YBRQ-_sz2CRAG.js} +1 -1
  52. package/dist/client/assets/{sizeCapture-X5ZJPWSS-Ba2xxyp_.js → sizeCapture-X5ZJPWSS-D0Q2QkYF.js} +1 -1
  53. package/dist/client/assets/{stateDiagram-2N3HPSRC-agPx5qAN.js → stateDiagram-2N3HPSRC-VgeSQTtW.js} +1 -1
  54. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +1 -0
  55. package/dist/client/assets/{swimlanes-5IMT3BWC-Cy6Lzj_L.js → swimlanes-5IMT3BWC-ClvBvSl0.js} +2 -2
  56. package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +8 -0
  57. package/dist/client/assets/{timeline-definition-FHXFAJF6-LBrVfbdp.js → timeline-definition-FHXFAJF6-BXNsrECV.js} +1 -1
  58. package/dist/client/assets/{vennDiagram-L72KCM5P-BiYIV7mX.js → vennDiagram-L72KCM5P-BgwiqZd-.js} +1 -1
  59. package/dist/client/assets/{wardleyDiagram-EHGQE667-Q1QU9gG3.js → wardleyDiagram-EHGQE667-DiqN5ERx.js} +1 -1
  60. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CzYpxk4l.js → xychartDiagram-FW5EYKEG-DQjFVqC6.js} +1 -1
  61. package/dist/client/index.html +2 -2
  62. package/dist/client/sw.js +1 -1
  63. package/dist/server/agent/autoTitle.js +95 -0
  64. package/dist/server/agent/installers.js +106 -22
  65. package/dist/server/agent/plugins.js +135 -4
  66. package/dist/server/agent/registry.js +2 -1
  67. package/dist/server/agent/session.js +27 -2
  68. package/dist/server/agent/titleNamerCatalog.js +301 -0
  69. package/dist/server/agentHook.js +49 -1
  70. package/dist/server/cli.js +238 -30
  71. package/dist/server/routes/filesystem.js +11 -8
  72. package/dist/server/routes/terminal.js +382 -14
  73. package/dist/server/utils/autoUpdate.js +175 -0
  74. package/dist/server/utils/npmUpdate.js +2 -1
  75. package/dist/server/utils/settings.js +43 -0
  76. package/package.json +1 -1
  77. package/runtime-manifest.json +2 -2
  78. package/dist/client/assets/channel-C2i7H7bH.js +0 -1
  79. package/dist/client/assets/classDiagram-OUVF2IWQ-BlGG6XFj.js +0 -1
  80. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BlGG6XFj.js +0 -1
  81. package/dist/client/assets/index-ChhyERKX.css +0 -1
  82. package/dist/client/assets/index-r-PkH7SB.js +0 -442
  83. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-beoQXpWJ.js +0 -1
  84. package/dist/client/assets/swimlanesDiagram-G3AALYLV-BxL1xNhC.js +0 -8
@@ -1,4 +1,4 @@
1
- import { app, BrowserWindow, dialog, ipcMain, Menu, net, Notification, shell, } from 'electron';
1
+ import { app, BrowserWindow, dialog, ipcMain, Menu, net, Notification, session, shell, } from 'electron';
2
2
  import { execFile, spawn } from 'node:child_process';
3
3
  import crypto from 'node:crypto';
4
4
  import fs from 'node:fs';
@@ -19,6 +19,7 @@ const DEFAULT_LOCAL_URL = 'http://localhost:9834';
19
19
  const PROTOCOL_VERSION = 1;
20
20
  const HEALTH_TIMEOUT_MS = 3_500;
21
21
  const START_TIMEOUT_MS = 90_000;
22
+ const localServiceCertificatePath = path.join(termdockDir, 'certs', 'termdock-local.pem');
22
23
  /** Delivered-but-unseen desktop notifications, mirrored into the Dock badge. */
23
24
  const activeNotifications = new Map();
24
25
  let unreadNotificationCount = 0;
@@ -27,11 +28,29 @@ function clearUnreadNotifications() {
27
28
  app.setBadgeCount(0);
28
29
  }
29
30
  let mainWindow = null;
30
- let activeServiceOrigin = null;
31
+ const serviceWindows = new Map();
32
+ const windowServiceOrigins = new WeakMap();
31
33
  let isQuitting = false;
34
+ function focusedWorkspaceWindow() {
35
+ const focused = BrowserWindow.getFocusedWindow();
36
+ if (focused && windowServiceOrigins.has(focused))
37
+ return focused;
38
+ return [...serviceWindows.values()].find((window) => !window.isDestroyed()) ?? null;
39
+ }
40
+ function serviceLabel(url) {
41
+ const normalized = normalizeServiceUrl(url);
42
+ const saved = readDesktopConfig().connections.find((entry) => entry.url === normalized);
43
+ if (saved?.label.trim())
44
+ return saved.label.trim();
45
+ const parsed = new URL(normalized);
46
+ return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'
47
+ ? '本机'
48
+ : parsed.host;
49
+ }
32
50
  function showDesktopMessageBox(options) {
33
- return mainWindow
34
- ? dialog.showMessageBox(mainWindow, options)
51
+ const parent = BrowserWindow.getFocusedWindow() ?? mainWindow;
52
+ return parent
53
+ ? dialog.showMessageBox(parent, options)
35
54
  : dialog.showMessageBox(options);
36
55
  }
37
56
  function openExternalLink(url) {
@@ -39,6 +58,34 @@ function openExternalLink(url) {
39
58
  return;
40
59
  void shell.openExternal(url);
41
60
  }
61
+ function configureLocalServiceCertificateTrust() {
62
+ let localFingerprint = null;
63
+ try {
64
+ localFingerprint = new crypto.X509Certificate(fs.readFileSync(localServiceCertificatePath)).fingerprint256;
65
+ }
66
+ catch {
67
+ // The certificate is created when the local service first enables HTTPS.
68
+ }
69
+ if (!localFingerprint)
70
+ return;
71
+ session.defaultSession.setCertificateVerifyProc((request, callback) => {
72
+ let isLocalTarget = false;
73
+ try {
74
+ isLocalTarget = isLocalNetworkTarget(new URL(`https://${request.hostname}`));
75
+ }
76
+ catch {
77
+ // Keep Chromium's default verification for malformed hostnames.
78
+ }
79
+ let presentedFingerprint = null;
80
+ try {
81
+ presentedFingerprint = new crypto.X509Certificate(request.certificate.data).fingerprint256;
82
+ }
83
+ catch {
84
+ // Keep Chromium's default verification if the certificate cannot be parsed.
85
+ }
86
+ callback(isLocalTarget && presentedFingerprint === localFingerprint ? 0 : -3);
87
+ });
88
+ }
42
89
  function defaultConfig() {
43
90
  return { version: 1, connections: [], lastConnectionUrl: null };
44
91
  }
@@ -221,6 +268,26 @@ async function probeService(rawUrl) {
221
268
  return { ok: false, url, error: message };
222
269
  }
223
270
  }
271
+ async function probeServiceWithLocalNetworkPermission(rawUrl) {
272
+ let probe = await probeService(rawUrl);
273
+ if (probe.ok)
274
+ return probe;
275
+ let parsedProbeUrl;
276
+ try {
277
+ parsedProbeUrl = new URL(probe.url);
278
+ }
279
+ catch {
280
+ return probe;
281
+ }
282
+ if (!isLocalNetworkTarget(parsedProbeUrl) || !looksLikeLocalNetworkPermissionError(probe.error)) {
283
+ return probe;
284
+ }
285
+ if (!(await requestLocalNetworkPermissionRetry(probe.url))) {
286
+ return { ...probe, error: 'Termdock 尚未获得 macOS 本地网络权限' };
287
+ }
288
+ probe = await probeService(rawUrl);
289
+ return probe;
290
+ }
224
291
  async function getLocalServiceStatus() {
225
292
  const state = readServerState();
226
293
  if (!state || !isProcessRunning(state.pid)) {
@@ -336,6 +403,8 @@ function desktopRuntimeEnv(runtime = runtimePaths()) {
336
403
  const currentPath = process.env.PATH ?? '/usr/bin:/bin:/usr/sbin:/sbin';
337
404
  return {
338
405
  ...process.env,
406
+ LANG: process.env.LANG || 'en_US.UTF-8',
407
+ LC_CTYPE: process.env.LC_CTYPE || process.env.LANG || 'en_US.UTF-8',
339
408
  PATH: [runtime.toolchainBin, '/opt/homebrew/bin', '/usr/local/bin', currentPath]
340
409
  .filter(Boolean)
341
410
  .join(path.delimiter),
@@ -364,6 +433,24 @@ async function waitForLocalService(childPid) {
364
433
  }
365
434
  return lastProbe;
366
435
  }
436
+ async function ensureTmuxUtf8Environment(runtime) {
437
+ const tmux = path.join(runtime.toolchainBin, 'tmux');
438
+ if (!fs.existsSync(tmux))
439
+ return;
440
+ const locale = desktopRuntimeEnv(runtime).LANG || 'en_US.UTF-8';
441
+ for (const name of ['LANG', 'LC_CTYPE']) {
442
+ try {
443
+ await execFileAsync(tmux, ['set-environment', '-g', name, locale], {
444
+ timeout: 5_000,
445
+ maxBuffer: 128 * 1024,
446
+ env: desktopRuntimeEnv(runtime),
447
+ });
448
+ }
449
+ catch {
450
+ // tmux is optional and may not have a running server yet.
451
+ }
452
+ }
453
+ }
367
454
  async function confirmAndStopExisting(status) {
368
455
  if (!status.state)
369
456
  return true;
@@ -460,6 +547,7 @@ async function startLocalService() {
460
547
  if (!probe.ok) {
461
548
  throw new Error(`桌面版服务启动失败:${probe.error ?? '未知错误'}。日志:${path.join(termdockDir, 'server.log')}`);
462
549
  }
550
+ await ensureTmuxUtf8Environment(runtime);
463
551
  await connectWindow(probe.url);
464
552
  return probe;
465
553
  }
@@ -504,51 +592,41 @@ async function installCli() {
504
592
  return snapshot();
505
593
  }
506
594
  async function connectWindow(rawUrl) {
507
- let probe = await probeService(rawUrl);
508
- if (!probe.ok) {
509
- let parsedProbeUrl = null;
510
- try {
511
- parsedProbeUrl = new URL(probe.url);
512
- }
513
- catch {
514
- // The probe already contains the normalized URL validation error.
515
- }
516
- if (parsedProbeUrl
517
- && isLocalNetworkTarget(parsedProbeUrl)
518
- && looksLikeLocalNetworkPermissionError(probe.error)) {
519
- const shouldRetry = await requestLocalNetworkPermissionRetry(probe.url);
520
- if (shouldRetry) {
521
- probe = await probeService(rawUrl);
522
- }
523
- else {
524
- return { ...probe, error: 'Termdock 尚未获得 macOS 本地网络权限' };
525
- }
526
- }
527
- }
595
+ let probe = await probeServiceWithLocalNetworkPermission(rawUrl);
528
596
  if (!probe.ok)
529
597
  return probe;
530
598
  const parsed = new URL(probe.url);
531
- while (mainWindow) {
532
- activeServiceOrigin = parsed.origin;
599
+ const key = parsed.origin;
600
+ const existingWindow = serviceWindows.get(key);
601
+ if (existingWindow && !existingWindow.isDestroyed()) {
602
+ existingWindow.show();
603
+ existingWindow.focus();
604
+ return probe;
605
+ }
606
+ const workspaceWindow = createDesktopWindow({ serviceOrigin: key, label: serviceLabel(probe.url) });
607
+ serviceWindows.set(key, workspaceWindow);
608
+ while (!workspaceWindow.isDestroyed()) {
533
609
  try {
534
- await mainWindow.loadURL(probe.url);
610
+ await workspaceWindow.loadURL(probe.url);
535
611
  const config = readDesktopConfig();
536
612
  config.lastConnectionUrl = probe.url;
537
613
  const existing = config.connections.find((entry) => entry.url === probe.url);
538
614
  if (existing)
539
615
  existing.lastConnectedAt = Date.now();
540
616
  writeDesktopConfig(config);
617
+ workspaceWindow.show();
618
+ workspaceWindow.focus();
541
619
  return probe;
542
620
  }
543
621
  catch (error) {
544
- activeServiceOrigin = null;
545
- await showConnectionCenter();
546
622
  const message = networkErrorDetails(error);
547
623
  if (isLocalNetworkTarget(parsed) && looksLikeLocalNetworkPermissionError(error)) {
548
624
  if (await requestLocalNetworkPermissionRetry(probe.url))
549
625
  continue;
626
+ workspaceWindow.destroy();
550
627
  return { ...probe, ok: false, error: 'Termdock 尚未获得 macOS 本地网络权限' };
551
628
  }
629
+ workspaceWindow.destroy();
552
630
  await showDesktopMessageBox({
553
631
  type: 'error',
554
632
  title: '连接 Termdock 服务失败',
@@ -561,7 +639,9 @@ async function connectWindow(rawUrl) {
561
639
  return { ...probe, ok: false, error: 'Termdock 窗口已关闭' };
562
640
  }
563
641
  async function showConnectionCenter() {
564
- activeServiceOrigin = null;
642
+ if (!mainWindow || mainWindow.isDestroyed()) {
643
+ mainWindow = createDesktopWindow();
644
+ }
565
645
  const rendererPath = app.isPackaged
566
646
  ? path.join(process.resourcesPath, 'renderer', 'index.html')
567
647
  : path.join(projectRoot, 'desktop', 'renderer', 'index.html');
@@ -569,10 +649,14 @@ async function showConnectionCenter() {
569
649
  mainWindow?.show();
570
650
  mainWindow?.focus();
571
651
  }
572
- function createMainWindow() {
652
+ function createDesktopWindow(options) {
573
653
  const window = new BrowserWindow({
574
- title: 'Termdock',
575
- width: 1280,
654
+ title: options ? `Termdock — ${options.label}` : 'Termdock — 连接中心',
655
+ show: false,
656
+ // The web workspace enables its persistent right inspector at 1440px.
657
+ // Open service windows above that breakpoint so the visible pin action
658
+ // cannot write a pinned state that the initial layout refuses to render.
659
+ width: options ? 1500 : 1280,
576
660
  height: 820,
577
661
  minWidth: 760,
578
662
  minHeight: 520,
@@ -586,6 +670,14 @@ function createMainWindow() {
586
670
  sandbox: true,
587
671
  },
588
672
  });
673
+ if (options)
674
+ windowServiceOrigins.set(window, options.serviceOrigin);
675
+ if (options) {
676
+ window.webContents.on('page-title-updated', (event) => {
677
+ event.preventDefault();
678
+ window.setTitle(`Termdock — ${options.label}`);
679
+ });
680
+ }
589
681
  window.webContents.setWindowOpenHandler(({ url }) => {
590
682
  if (isExternalLinkStagingUrl(url)) {
591
683
  return {
@@ -614,10 +706,11 @@ function createMainWindow() {
614
706
  });
615
707
  });
616
708
  window.webContents.on('will-navigate', (event, url) => {
617
- if (!activeServiceOrigin)
709
+ const serviceOrigin = windowServiceOrigins.get(window);
710
+ if (!serviceOrigin)
618
711
  return;
619
712
  try {
620
- if (new URL(url).origin === activeServiceOrigin)
713
+ if (new URL(url).origin === serviceOrigin)
621
714
  return;
622
715
  }
623
716
  catch {
@@ -627,24 +720,83 @@ function createMainWindow() {
627
720
  openExternalLink(url);
628
721
  });
629
722
  window.webContents.on('did-finish-load', () => {
630
- if (!activeServiceOrigin)
723
+ if (!windowServiceOrigins.has(window))
631
724
  return;
632
725
  void window.webContents.insertCSS(`
633
726
  html[data-termdock-desktop='true'] {
634
727
  --safe-top-inset: max(env(safe-area-inset-top, 0px), 38px) !important;
635
728
  }
729
+ html[data-termdock-desktop='true'] #root {
730
+ padding-top: 0 !important;
731
+ }
732
+ html[data-termdock-desktop='true'] body::after {
733
+ display: none !important;
734
+ }
735
+ html[data-termdock-desktop='true'] #root main > div > div:first-child {
736
+ padding-left: 80px !important;
737
+ position: relative;
738
+ -webkit-app-region: no-drag;
739
+ }
740
+ html[data-termdock-desktop='true'] #root main > div > div:first-child::before {
741
+ content: '';
742
+ position: absolute;
743
+ top: 0;
744
+ bottom: 0;
745
+ left: 50%;
746
+ width: min(240px, 25vw);
747
+ transform: translateX(-50%);
748
+ pointer-events: none;
749
+ -webkit-app-region: drag;
750
+ }
751
+ html[data-termdock-desktop='true'] #root .h-full.flex.flex-col.app-chrome-bg.border-r > .shrink-0.border-b {
752
+ padding-left: 80px !important;
753
+ -webkit-app-region: drag;
754
+ }
755
+ html[data-termdock-desktop='true'] #root button,
756
+ html[data-termdock-desktop='true'] #root a,
757
+ html[data-termdock-desktop='true'] #root input,
758
+ html[data-termdock-desktop='true'] #root textarea,
759
+ html[data-termdock-desktop='true'] #root select,
760
+ html[data-termdock-desktop='true'] #root [data-rbd-draggable-id] {
761
+ -webkit-app-region: no-drag;
762
+ }
763
+ html[data-termdock-desktop='true'] [data-sidebar='right'] {
764
+ padding-top: 0 !important;
765
+ }
766
+ html[data-termdock-desktop='true'] [data-sidebar='right'][style*='pointer-events: auto'] {
767
+ transform: none !important;
768
+ }
769
+ html[data-termdock-desktop='true'] [data-sidebar='right'] > div:first-child {
770
+ -webkit-app-region: no-drag;
771
+ }
772
+ html[data-termdock-desktop='true'] [data-sidebar='right'] > div:first-child > div:first-child > .min-w-0.flex-1 {
773
+ -webkit-app-region: drag;
774
+ }
775
+ html[data-termdock-desktop='true'] [data-sidebar='right'] div:has(> [data-right-search]) + div:has(> button[aria-pressed]) {
776
+ width: fit-content !important;
777
+ margin-left: auto !important;
778
+ }
779
+ html[data-termdock-desktop='true'] [data-sidebar='right'] div:has(> [data-right-search]) + div:has(> button[aria-pressed]) > button {
780
+ flex: none !important;
781
+ padding: 0.125rem 0.5rem !important;
782
+ font-size: 10px !important;
783
+ }
636
784
  `);
637
785
  });
638
786
  window.on('closed', () => {
639
787
  if (mainWindow === window)
640
788
  mainWindow = null;
789
+ const serviceOrigin = windowServiceOrigins.get(window);
790
+ if (serviceOrigin && serviceWindows.get(serviceOrigin) === window) {
791
+ serviceWindows.delete(serviceOrigin);
792
+ }
641
793
  });
642
794
  window.on('focus', () => {
643
795
  // User is looking at the app — the Dock badge has served its purpose.
644
796
  clearUnreadNotifications();
645
797
  });
646
798
  window.on('close', (event) => {
647
- if (process.platform === 'darwin' && !isQuitting) {
799
+ if (window === mainWindow && process.platform === 'darwin' && !isQuitting) {
648
800
  event.preventDefault();
649
801
  window.hide();
650
802
  }
@@ -653,7 +805,7 @@ function createMainWindow() {
653
805
  }
654
806
  function installIpcHandlers() {
655
807
  ipcMain.handle('desktop:snapshot', () => snapshot());
656
- ipcMain.handle('desktop:probe', (_event, url) => probeService(url));
808
+ ipcMain.handle('desktop:probe', (_event, url) => probeServiceWithLocalNetworkPermission(url));
657
809
  ipcMain.handle('desktop:save-connection', async (_event, input) => {
658
810
  const url = normalizeServiceUrl(input.url);
659
811
  const config = readDesktopConfig();
@@ -669,12 +821,14 @@ function installIpcHandlers() {
669
821
  });
670
822
  }
671
823
  writeDesktopConfig(config);
824
+ installMenu();
672
825
  return snapshot();
673
826
  });
674
827
  ipcMain.handle('desktop:remove-connection', async (_event, id) => {
675
828
  const config = readDesktopConfig();
676
829
  config.connections = config.connections.filter((entry) => entry.id !== id);
677
830
  writeDesktopConfig(config);
831
+ installMenu();
678
832
  return snapshot();
679
833
  });
680
834
  ipcMain.handle('desktop:connect', (_event, url) => connectWindow(url));
@@ -687,7 +841,7 @@ function installIpcHandlers() {
687
841
  if (error)
688
842
  throw new Error(error);
689
843
  });
690
- ipcMain.handle('desktop:show-notification', (_event, payload) => {
844
+ ipcMain.handle('desktop:show-notification', (event, payload) => {
691
845
  if (!Notification.isSupported() || typeof payload?.title !== 'string')
692
846
  return false;
693
847
  // Web Notification tag semantics: a new notification with the same tag
@@ -712,11 +866,15 @@ function installIpcHandlers() {
712
866
  activeNotifications.delete(tag);
713
867
  });
714
868
  }
869
+ const sourceWindow = BrowserWindow.fromWebContents(event.sender);
715
870
  notification.on('click', () => {
716
- mainWindow?.show();
717
- mainWindow?.focus();
871
+ const targetWindow = sourceWindow && !sourceWindow.isDestroyed()
872
+ ? sourceWindow
873
+ : focusedWorkspaceWindow();
874
+ targetWindow?.show();
875
+ targetWindow?.focus();
718
876
  if (typeof payload.sessionId === 'string') {
719
- mainWindow?.webContents.send('desktop:focus-session', payload.sessionId);
877
+ targetWindow?.webContents.send('desktop:focus-session', payload.sessionId);
720
878
  }
721
879
  });
722
880
  notification.show();
@@ -724,7 +882,7 @@ function installIpcHandlers() {
724
882
  app.setBadgeCount(unreadNotificationCount);
725
883
  // macOS banners auto-dismiss even for "persistent" alerts; bounce the Dock
726
884
  // icon so the signal survives until the user looks at the app.
727
- if (payload.persistent === true && !mainWindow?.isFocused()) {
885
+ if (payload.persistent === true && !sourceWindow?.isFocused()) {
728
886
  app.dock?.bounce('informational');
729
887
  }
730
888
  return true;
@@ -732,8 +890,37 @@ function installIpcHandlers() {
732
890
  }
733
891
  function installMenu() {
734
892
  const sendCommand = (command) => {
735
- mainWindow?.webContents.send('desktop:command', command);
893
+ focusedWorkspaceWindow()?.webContents.send('desktop:command', command);
894
+ };
895
+ const openService = (url) => {
896
+ void connectWindow(url).then((probe) => {
897
+ if (!probe.ok) {
898
+ void showDesktopMessageBox({
899
+ type: 'error',
900
+ title: '连接 Termdock 服务失败',
901
+ message: `无法打开 ${probe.url}`,
902
+ detail: probe.error,
903
+ });
904
+ }
905
+ });
906
+ };
907
+ const openLocalService = () => {
908
+ void getLocalServiceStatus().then((status) => {
909
+ if (status.running && status.state) {
910
+ openService(stateUrl(status.state));
911
+ return;
912
+ }
913
+ void showConnectionCenter();
914
+ });
736
915
  };
916
+ const config = readDesktopConfig();
917
+ const serviceItems = [
918
+ { label: '本机', click: openLocalService },
919
+ ...config.connections.map((connection) => ({
920
+ label: connection.label || new URL(connection.url).host,
921
+ click: () => openService(connection.url),
922
+ })),
923
+ ];
737
924
  const menu = Menu.buildFromTemplate([
738
925
  {
739
926
  label: 'Termdock',
@@ -743,13 +930,14 @@ function installMenu() {
743
930
  {
744
931
  label: '设置…',
745
932
  accelerator: 'CmdOrCtrl+,',
746
- click: () => mainWindow?.webContents.send('desktop:open-settings'),
933
+ click: () => focusedWorkspaceWindow()?.webContents.send('desktop:open-settings'),
747
934
  },
748
935
  {
749
936
  label: '连接中心',
750
- accelerator: 'CmdOrCtrl+Shift+O',
937
+ accelerator: 'CmdOrCtrl+N',
751
938
  click: () => void showConnectionCenter(),
752
939
  },
940
+ { label: '打开服务', submenu: serviceItems },
753
941
  {
754
942
  label: '安装或修复 CLI…',
755
943
  click: () => void installCli().catch((error) => {
@@ -855,6 +1043,13 @@ function installMenu() {
855
1043
  {
856
1044
  label: '窗口',
857
1045
  submenu: [
1046
+ {
1047
+ label: '连接中心',
1048
+ accelerator: 'CmdOrCtrl+Shift+O',
1049
+ click: () => void showConnectionCenter(),
1050
+ },
1051
+ { label: '打开服务', submenu: serviceItems },
1052
+ { type: 'separator' },
858
1053
  { role: 'minimize' },
859
1054
  { role: 'zoom' },
860
1055
  { role: 'front' },
@@ -862,11 +1057,17 @@ function installMenu() {
862
1057
  },
863
1058
  ]);
864
1059
  Menu.setApplicationMenu(menu);
1060
+ app.dock?.setMenu(Menu.buildFromTemplate([
1061
+ { label: '连接中心', click: () => void showConnectionCenter() },
1062
+ { type: 'separator' },
1063
+ ...serviceItems,
1064
+ ]));
865
1065
  }
866
1066
  app.whenReady().then(async () => {
1067
+ configureLocalServiceCertificateTrust();
867
1068
  installIpcHandlers();
868
1069
  installMenu();
869
- mainWindow = createMainWindow();
1070
+ mainWindow = createDesktopWindow();
870
1071
  configureDesktopUpdater(showDesktopMessageBox);
871
1072
  await showConnectionCenter();
872
1073
  const lastConnectionUrl = readDesktopConfig().lastConnectionUrl;
@@ -878,9 +1079,13 @@ app.whenReady().then(async () => {
878
1079
  }
879
1080
  app.on('activate', () => {
880
1081
  if (!mainWindow) {
881
- mainWindow = createMainWindow();
1082
+ mainWindow = createDesktopWindow();
882
1083
  void showConnectionCenter();
883
1084
  }
1085
+ else {
1086
+ mainWindow.show();
1087
+ mainWindow.focus();
1088
+ }
884
1089
  });
885
1090
  }).catch((error) => {
886
1091
  void dialog.showErrorBox('Termdock 启动失败', error instanceof Error ? error.message : String(error));
package/README.md CHANGED
@@ -91,6 +91,12 @@ td update # 从 npm 官方源升级全局 CLI
91
91
  如果后台服务正在运行,升级不会打断现有终端会话;可在方便时执行
92
92
  `td --stop && td`,让服务切换到新版本。
93
93
 
94
+ npm CLI 服务也会自动更新:启动 15 秒后检查一次,之后每 6 小时检查。
95
+ 新版会在后台安装,但无论是否有网页连接都绝不会自动重启。更新状态会持久保存,
96
+ 用户打开网页后可在左侧边栏“更多”按钮看到更新圆点和提醒;只有用户明确确认后
97
+ 才会重启,未确认的提醒会一直保留。tmux 和由 PTY host 托管的会话可跨此次重启继续使用。
98
+ macOS 桌面版仍使用独立的签名更新机制,不运行 npm 自动更新器。
99
+
94
100
  ### 设置访问密码(强烈推荐)
95
101
 
96
102
  如果服务暴露在 LAN 上,**务必先设置密码**,否则任何能访问到主机/端口的人都能执行 shell 命令: