@xulongzhe/clawbench-desktop-linux-x64-payload 0.104.0 → 0.106.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xulongzhe/clawbench-desktop-linux-x64-payload",
3
- "version": "0.104.0",
3
+ "version": "0.106.0",
4
4
  "description": "ClawBench desktop client — Linux x64 incremental-upgrade payload (app files only, no Electron runtime)",
5
5
  "files": [
6
6
  "payload.json",
Binary file
@@ -13,9 +13,10 @@
13
13
  password_label: 'Password',
14
14
  password_placeholder: 'Password',
15
15
  connect: 'Connect',
16
- protocol: 'Protocol',
17
16
  server_address: 'Server Address',
18
- add_connect: 'Add & Connect',
17
+ add: 'Add',
18
+ save: 'Save',
19
+ edit_server: 'Edit Server',
19
20
  cancel: 'Cancel',
20
21
  add_server: 'Add Server',
21
22
  confirm: 'Confirm',
@@ -24,8 +25,13 @@
24
25
  delete_server_msg: 'Delete this server?',
25
26
  saved_servers: 'Saved Servers',
26
27
  authenticating: 'Authenticating...',
28
+ saving: 'Saving...',
29
+ name_label: 'Name (optional)',
30
+ name_placeholder: 'e.g. Home NAS',
31
+ duplicate_name: 'That name is already used',
27
32
  enter_password: 'Please enter password',
28
- enter_address: 'Please enter server address',
33
+ enter_full_address: 'Enter a full address with http:// or https:// and a port, e.g. https://192.168.1.100:20000',
34
+ duplicate_server: 'That address is already saved',
29
35
  connection_failed: 'Connection failed: ',
30
36
  connect_error: 'Unable to connect. Check address and network.',
31
37
  wrong_password: 'Wrong password',
@@ -36,7 +42,17 @@
36
42
  splash_loading: 'Loading page…',
37
43
  splash_rendering: 'Rendering…',
38
44
  splash_initializing: 'Initializing app…',
39
- splash_cancel: 'Cancel connection'
45
+ splash_cancel: 'Cancel connection',
46
+ window_close: 'Close',
47
+ // Version-mismatch gate. The desktop and server versions differ in either
48
+ // direction, so the wording must not assume the app is the older side —
49
+ // the two `_older`/`_newer` variants cover that.
50
+ version_mismatch_title: 'Version mismatch',
51
+ version_mismatch_body: 'Desktop version: %1\nServer version: %2\n\nThe two versions may be incompatible: some features may behave unexpectedly or stop working.',
52
+ version_mismatch_older: 'The desktop app is older than the server. Updating is recommended.',
53
+ version_mismatch_newer: 'The desktop app is newer than the server. Downloading will align it to the server version (a downgrade).',
54
+ version_mismatch_download: 'Download',
55
+ version_mismatch_continue: 'Continue anyway'
40
56
  },
41
57
  zh: {
42
58
  slogan: '从终端到桌面',
@@ -44,9 +60,10 @@
44
60
  password_label: '登录密码',
45
61
  password_placeholder: '密码',
46
62
  connect: '连 接',
47
- protocol: '协议',
48
63
  server_address: '服务器地址',
49
- add_connect: '添 加 并 连 接',
64
+ add: '添 加',
65
+ save: '保 存',
66
+ edit_server: '编辑服务器',
50
67
  cancel: '取 消',
51
68
  add_server: '添加服务器',
52
69
  confirm: '确认',
@@ -55,8 +72,13 @@
55
72
  delete_server_msg: '删除此服务器?',
56
73
  saved_servers: '已保存服务器',
57
74
  authenticating: '认证中...',
75
+ saving: '保存中...',
76
+ name_label: '名称(选填)',
77
+ name_placeholder: '例如 家里的 NAS',
78
+ duplicate_name: '该名称已被使用',
58
79
  enter_password: '请输入密码',
59
- enter_address: '请输入服务器地址',
80
+ enter_full_address: '请输入完整地址,包含 http:// 或 https:// 与端口,例如 https://192.168.1.100:20000',
81
+ duplicate_server: '该地址已存在',
60
82
  connection_failed: '连接失败: ',
61
83
  connect_error: '无法连接到服务器,请检查地址和网络。',
62
84
  wrong_password: '密码错误',
@@ -67,7 +89,16 @@
67
89
  splash_loading: '正在加载页面…',
68
90
  splash_rendering: '正在渲染…',
69
91
  splash_initializing: '正在初始化应用…',
70
- splash_cancel: '取消连接'
92
+ splash_cancel: '取消连接',
93
+ window_close: '关闭',
94
+ // Version-mismatch gate. See the en block for why the wording is split by
95
+ // direction rather than assuming the app is older.
96
+ version_mismatch_title: '版本不一致',
97
+ version_mismatch_body: '桌面端版本: %1\n服务器版本: %2\n\n两者可能不兼容:部分功能可能异常或无法使用。',
98
+ version_mismatch_older: '桌面端版本低于服务器,建议更新。',
99
+ version_mismatch_newer: '桌面端版本高于服务器,下载将对齐到服务器版本(降级)。',
100
+ version_mismatch_download: '下载',
101
+ version_mismatch_continue: '仍然继续'
71
102
  }
72
103
  };
73
104
  var LANG = 'en';
@@ -715,6 +746,76 @@
715
746
  position: relative;
716
747
  overflow: hidden;
717
748
  -webkit-tap-highlight-color: transparent;
749
+ /* This page IS the window on first run and whenever the server is
750
+ unreachable, and the window is frameless on Windows/Linux — so without a
751
+ drag region the login window could not be moved at all. The whole page is
752
+ the handle (there is no title bar), which is why every interactive
753
+ element below has to opt back out. Ignored on macOS/Android/browser. */
754
+ -webkit-app-region: drag;
755
+ -webkit-user-select: none;
756
+ user-select: none;
757
+ }
758
+
759
+ /* Opt out of the page-wide drag handle. Without this the inputs and buttons
760
+ would be undraggable-but-unclickable — a click would start a window move.
761
+ `.dlg-box` is included because the confirmation dialog's message text
762
+ should be selectable, and `.server-item` because its rows are clickable. */
763
+ input,
764
+ button,
765
+ select,
766
+ a,
767
+ .form-card,
768
+ .server-list,
769
+ .field-group,
770
+ .server-item,
771
+ .dlg-box,
772
+ .dlg-actions {
773
+ -webkit-app-region: no-drag;
774
+ }
775
+
776
+ /* Text the user may want to select (server URLs, error messages) opts out of
777
+ both the drag region and the inherited user-select:none. */
778
+ input,
779
+ .server-url,
780
+ .error-message,
781
+ .splash-stage {
782
+ -webkit-user-select: text;
783
+ user-select: text;
784
+ }
785
+
786
+ /* Window close, for the frameless Windows/Linux shell. Pinned to the window's
787
+ top-right — the same corner the native controls occupy on those platforms. */
788
+ .window-close {
789
+ position: fixed;
790
+ top: 0;
791
+ right: 0;
792
+ width: 46px;
793
+ height: 32px;
794
+ display: flex;
795
+ align-items: center;
796
+ justify-content: center;
797
+ padding: 0;
798
+ border: none;
799
+ border-radius: 0 0 0 6px;
800
+ background: transparent;
801
+ color: var(--text-hint);
802
+ cursor: pointer;
803
+ z-index: 200;
804
+ transition: background 150ms ease, color 150ms ease;
805
+ }
806
+
807
+ .window-close svg {
808
+ width: 15px;
809
+ height: 15px;
810
+ }
811
+
812
+ .window-close:hover {
813
+ background: var(--color-red, #ef4444);
814
+ color: #fff;
815
+ }
816
+
817
+ .window-close[hidden] {
818
+ display: none;
718
819
  }
719
820
 
720
821
  /* Decorative background — matches LoginView.vue */
@@ -922,6 +1023,16 @@
922
1023
  font-weight: 500;
923
1024
  }
924
1025
 
1026
+ /* Row actions: edit + delete, kept in one group so the info block can
1027
+ stretch without pushing the icons apart. */
1028
+ .server-item-actions {
1029
+ flex-shrink: 0;
1030
+ display: flex;
1031
+ align-items: center;
1032
+ gap: 2px;
1033
+ }
1034
+
1035
+ .server-edit-btn,
925
1036
  .server-delete-btn {
926
1037
  flex-shrink: 0;
927
1038
  border: none;
@@ -936,15 +1047,26 @@
936
1047
  transition: background 0.1s, color 0.1s;
937
1048
  }
938
1049
 
1050
+ .server-edit-btn:hover {
1051
+ background: rgba(var(--accent-rgb), 0.12);
1052
+ color: var(--accent-color);
1053
+ }
1054
+
939
1055
  .server-delete-btn:hover {
940
1056
  background: rgba(220, 38, 38, 0.1);
941
1057
  color: var(--color-red);
942
1058
  }
943
1059
 
1060
+ .server-item.active .server-edit-btn,
944
1061
  .server-item.active .server-delete-btn {
945
1062
  color: var(--text-muted);
946
1063
  }
947
1064
 
1065
+ .server-item.active .server-edit-btn:hover {
1066
+ color: var(--accent-color);
1067
+ background: rgba(var(--accent-rgb), 0.15);
1068
+ }
1069
+
948
1070
  .server-item.active .server-delete-btn:hover {
949
1071
  color: var(--color-red);
950
1072
  background: rgba(220, 38, 38, 0.12);
@@ -965,69 +1087,12 @@
965
1087
  margin-bottom: 6px;
966
1088
  }
967
1089
 
968
- /* Protocol radio */
969
- .protocol-group {
970
- display: flex;
971
- gap: 20px;
972
- margin-bottom: 16px;
973
- }
974
-
975
- .protocol-group label {
976
- display: flex;
977
- align-items: center;
978
- gap: 6px;
979
- font-size: 14px;
1090
+ /* Title shown above the add form when it is reused for editing */
1091
+ .form-title {
1092
+ font-size: 15px;
1093
+ font-weight: 600;
980
1094
  color: var(--text-primary);
981
- cursor: pointer;
982
- }
983
-
984
- .protocol-group input[type="radio"] {
985
- appearance: none;
986
- -webkit-appearance: none;
987
- width: 18px;
988
- height: 18px;
989
- border: 2px solid var(--border-color);
990
- border-radius: 50%;
991
- position: relative;
992
- cursor: pointer;
993
- transition: border-color 0.2s;
994
- }
995
-
996
- .protocol-group input[type="radio"]:checked {
997
- border-color: var(--accent-color);
998
- }
999
-
1000
- .protocol-group input[type="radio"]:checked::after {
1001
- content: '';
1002
- position: absolute;
1003
- top: 3px;
1004
- left: 3px;
1005
- width: 8px;
1006
- height: 8px;
1007
- border-radius: 50%;
1008
- background: var(--accent-color);
1009
- }
1010
-
1011
- /* Host + Port row */
1012
- .host-row {
1013
- display: flex;
1014
- align-items: center;
1015
- gap: 4px;
1016
- }
1017
-
1018
- .host-row input:first-child {
1019
- flex: 1;
1020
- }
1021
-
1022
- .host-row .colon {
1023
- color: var(--text-muted);
1024
- font-size: 18px;
1025
- line-height: 44px;
1026
- }
1027
-
1028
- .host-row input:last-child {
1029
- width: 90px;
1030
- flex-shrink: 0;
1095
+ margin-bottom: 14px;
1031
1096
  }
1032
1097
 
1033
1098
  /* Input fields — matches LoginView.vue input style */
@@ -1386,6 +1451,94 @@
1386
1451
  background: var(--bg-tertiary);
1387
1452
  color: var(--text-primary);
1388
1453
  }
1454
+
1455
+ /* ── Version-mismatch gate ──────────────────────────────────────────────
1456
+ Replaces the loading splash when the desktop/server versions differ. Like
1457
+ the splash it is hidden unless a body class is set (added by
1458
+ __versionMismatchShow), so the same document can serve the login form, the
1459
+ splash and the gate. Styled as a card, mirroring the app's DialogOverlay so
1460
+ the prompt reads as the same UI as the in-app dialogs. */
1461
+ .gate { display: none; }
1462
+
1463
+ body.gate-mode .gate {
1464
+ position: fixed;
1465
+ inset: 0;
1466
+ /* Above the splash: the gate replaces it, but both may be display:flex at
1467
+ once (the page is reused and the main process does not always clear
1468
+ splash-mode), so the gate must win the paint order. */
1469
+ z-index: 200;
1470
+ display: flex;
1471
+ align-items: center;
1472
+ justify-content: center;
1473
+ padding: 0 20px;
1474
+ background: var(--bg-primary);
1475
+ }
1476
+
1477
+ body.gate-mode .content { display: none; }
1478
+
1479
+ .gate-card {
1480
+ width: 100%;
1481
+ max-width: 360px;
1482
+ padding: 18px 20px 14px;
1483
+ background: var(--bg-secondary);
1484
+ border-radius: 14px;
1485
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16), 0 14px 44px rgba(0, 0, 0, 0.3);
1486
+ }
1487
+
1488
+ .gate-title {
1489
+ font-size: 15px;
1490
+ font-weight: 600;
1491
+ color: var(--text-primary);
1492
+ margin-bottom: 8px;
1493
+ }
1494
+
1495
+ .gate-body {
1496
+ font-size: 13px;
1497
+ line-height: 1.6;
1498
+ color: var(--text-secondary);
1499
+ white-space: pre-line;
1500
+ word-break: break-word;
1501
+ }
1502
+
1503
+ .gate-direction {
1504
+ margin-top: 10px;
1505
+ font-size: 12px;
1506
+ line-height: 1.5;
1507
+ color: var(--text-muted);
1508
+ }
1509
+
1510
+ .gate-actions {
1511
+ display: flex;
1512
+ justify-content: flex-end;
1513
+ gap: 8px;
1514
+ margin-top: 16px;
1515
+ }
1516
+
1517
+ .gate-btn {
1518
+ padding: 7px 14px;
1519
+ font-size: 13px;
1520
+ font-family: inherit;
1521
+ border-radius: 6px;
1522
+ cursor: pointer;
1523
+ transition: opacity 150ms ease, background 150ms ease, color 150ms ease;
1524
+ }
1525
+
1526
+ .gate-continue {
1527
+ color: var(--text-secondary);
1528
+ background: var(--bg-tertiary);
1529
+ border: 1px solid var(--border-color);
1530
+ }
1531
+ .gate-continue:hover {
1532
+ color: var(--text-primary);
1533
+ }
1534
+
1535
+ .gate-download {
1536
+ color: #fff;
1537
+ background: var(--accent-color);
1538
+ border: none;
1539
+ font-weight: 600;
1540
+ }
1541
+ .gate-download:hover { opacity: 0.9; }
1389
1542
  </style>
1390
1543
  </head>
1391
1544
  <body>
@@ -1402,9 +1555,38 @@
1402
1555
  <button type="button" class="splash-cancel" id="splashCancel" data-i18n="splash_cancel">取消连接</button>
1403
1556
  </div>
1404
1557
 
1558
+ <!-- Version-mismatch gate. Shown by the main process (see splash.ts /
1559
+ versionGate.ts) INSTEAD of the loading splash when the desktop and server
1560
+ versions differ in either direction. Blocking: it has no cancel button, so
1561
+ the only ways out are "download" and "continue". -->
1562
+ <div class="gate" id="versionGate">
1563
+ <div class="gate-card">
1564
+ <div class="gate-title" id="gateTitle" data-i18n="version_mismatch_title">版本不一致</div>
1565
+ <div class="gate-body" id="gateBody"></div>
1566
+ <div class="gate-direction" id="gateDirection"></div>
1567
+ <div class="gate-actions">
1568
+ <button type="button" class="gate-btn gate-continue" id="gateContinue" data-i18n="version_mismatch_continue">仍然继续</button>
1569
+ <button type="button" class="gate-btn gate-download" id="gateDownload" data-i18n="version_mismatch_download">下载</button>
1570
+ </div>
1571
+ </div>
1572
+ </div>
1573
+
1405
1574
  <div class="bg-gradient"></div>
1406
1575
  <div class="bg-grid"></div>
1407
1576
 
1577
+ <!-- Window close, for the frameless Windows/Linux shell.
1578
+ This document has no AppHeader, so it is the ONLY way to close the window
1579
+ on first run or when the server is unreachable — without it the window
1580
+ would have no close affordance at all. Hidden unless the host reports
1581
+ app-drawn controls (macOS keeps its traffic lights, and Android is not a
1582
+ window), so it never duplicates a native button. -->
1583
+ <button type="button" class="window-close" id="windowClose" data-i18n-aria-label="window_close" data-i18n-title="window_close" aria-label="Close" title="Close" hidden>
1584
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1585
+ <line x1="6" y1="6" x2="18" y2="18"/>
1586
+ <line x1="18" y1="6" x2="6" y2="18"/>
1587
+ </svg>
1588
+ </button>
1589
+
1408
1590
  <div class="content">
1409
1591
  <!-- Brand -->
1410
1592
  <div class="brand">
@@ -1453,31 +1635,20 @@
1453
1635
  </div>
1454
1636
  </form>
1455
1637
 
1456
- <!-- Add server form (hidden by default) -->
1638
+ <!-- Add / edit server form (hidden by default) -->
1457
1639
  <form id="addServerForm" onsubmit="return false;" style="display:none;">
1458
- <!-- Protocol -->
1640
+ <div class="form-title" id="addFormTitle" style="display:none;"></div>
1641
+
1642
+ <!-- Optional display name -->
1459
1643
  <div class="field-group">
1460
- <span class="field-label" data-i18n="protocol">协议</span>
1461
- <div class="protocol-group">
1462
- <label>
1463
- <input type="radio" name="addProtocol" value="https" checked>
1464
- HTTPS
1465
- </label>
1466
- <label>
1467
- <input type="radio" name="addProtocol" value="http">
1468
- HTTP
1469
- </label>
1470
- </div>
1644
+ <span class="field-label" data-i18n="name_label">名称(选填)</span>
1645
+ <input type="text" id="addName" data-i18n-placeholder="name_placeholder" placeholder="例如 家里的 NAS" maxlength="40" autocorrect="off" autocapitalize="sentences" spellcheck="false">
1471
1646
  </div>
1472
1647
 
1473
- <!-- Host + Port -->
1648
+ <!-- Server address (protocol + host + port in one field) -->
1474
1649
  <div class="field-group">
1475
1650
  <span class="field-label" data-i18n="server_address">服务器地址</span>
1476
- <div class="host-row">
1477
- <input type="text" id="addHost" placeholder="192.168.1.100" autocorrect="off" autocapitalize="none" spellcheck="false">
1478
- <span class="colon">:</span>
1479
- <input type="number" id="addPort" value="20000" inputmode="numeric">
1480
- </div>
1651
+ <input type="text" id="addAddress" placeholder="https://192.168.1.100:20000" autocorrect="off" autocapitalize="none" spellcheck="false" inputmode="url">
1481
1652
  </div>
1482
1653
 
1483
1654
  <!-- Password -->
@@ -1493,7 +1664,7 @@
1493
1664
  </div>
1494
1665
 
1495
1666
  <button type="submit" class="connect-btn" id="addConnectBtn">
1496
- <span id="addBtnText" data-i18n="add_connect">添 加 并 连 接</span>
1667
+ <span id="addBtnText" data-i18n="add">添 加</span>
1497
1668
  </button>
1498
1669
  <button type="button" class="cancel-btn" id="cancelAddBtn" data-i18n="cancel">取 消</button>
1499
1670
 
@@ -1537,6 +1708,7 @@
1537
1708
  // SVG icons
1538
1709
  var SVG_SERVER = '<svg class="server-item-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>';
1539
1710
  var SVG_X = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
1711
+ var SVG_PENCIL = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>';
1540
1712
 
1541
1713
  function applyI18n() {
1542
1714
  document.querySelectorAll('[data-i18n]').forEach(function(el) {
@@ -1545,6 +1717,15 @@
1545
1717
  document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
1546
1718
  el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
1547
1719
  });
1720
+ // Attribute-valued strings (tooltips, accessible names). The icon-only
1721
+ // close button has no text to translate, so without these it would keep its
1722
+ // hard-coded English label in every language.
1723
+ document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
1724
+ el.title = t(el.getAttribute('data-i18n-title'));
1725
+ });
1726
+ document.querySelectorAll('[data-i18n-aria-label]').forEach(function(el) {
1727
+ el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria-label')));
1728
+ });
1548
1729
  }
1549
1730
 
1550
1731
  // ── Splash overlay (?splash=1) ──────────────────────────────────────────
@@ -1585,10 +1766,60 @@
1585
1766
  if (el) el.classList.remove('is-fading');
1586
1767
  };
1587
1768
 
1769
+ // ── Version-mismatch gate ───────────────────────────────────────────────
1770
+ // Shown INSTEAD of the loading splash when the desktop and server versions
1771
+ // differ in either direction (see desktop/src/main/versionGate.ts). The main
1772
+ // process calls __versionMismatchShow with the two versions and which side is
1773
+ // newer; the buttons report the user's choice back through the native bridge.
1774
+ //
1775
+ // The direction decides the wording: "newer" means the desktop app is AHEAD,
1776
+ // where downloading aligns DOWN to the server — so "update" would be wrong.
1777
+ /** Fill the gate with the version details. Called from the main process. */
1778
+ window.__versionMismatchShow = function(info) {
1779
+ if (!info) return;
1780
+ var body = document.getElementById('gateBody');
1781
+ if (body) {
1782
+ body.textContent = t('version_mismatch_body')
1783
+ .replace('%1', info.clientVersion || '')
1784
+ .replace('%2', info.serverVersion || '');
1785
+ }
1786
+ var dir = document.getElementById('gateDirection');
1787
+ if (dir) {
1788
+ dir.textContent = t(info.direction === 'newer'
1789
+ ? 'version_mismatch_newer'
1790
+ : 'version_mismatch_older');
1791
+ }
1792
+ document.body.classList.add('gate-mode');
1793
+ };
1794
+
1795
+ /** Clear the gate state. The page is REUSED, so this runs before every show. */
1796
+ window.__versionMismatchReset = function() {
1797
+ document.body.classList.remove('gate-mode');
1798
+ };
1799
+
1588
1800
  if (SPLASH_MODE) {
1589
1801
  document.body.classList.add('splash-mode');
1590
1802
  }
1591
1803
 
1804
+ /**
1805
+ * Reveal and wire the window close button when the host draws its own
1806
+ * controls (frameless Windows/Linux). `hasCustomWindowControls` is absent on
1807
+ * older hosts, macOS and Android — the button stays hidden there, so it never
1808
+ * duplicates a native close button. Runs before DOMContentLoaded so the
1809
+ * button is usable even while the splash overlay is up.
1810
+ */
1811
+ (function initWindowClose() {
1812
+ var btn = document.getElementById('windowClose');
1813
+ if (!btn) return;
1814
+ var supported = false;
1815
+ try { supported = ClawBenchNative.hasCustomWindowControls && ClawBenchNative.hasCustomWindowControls() === true; } catch(e) {}
1816
+ if (!supported) return;
1817
+ btn.hidden = false;
1818
+ btn.addEventListener('click', function() {
1819
+ try { ClawBenchNative.windowClose(); } catch(e) {}
1820
+ });
1821
+ })();
1822
+
1592
1823
  window.addEventListener('DOMContentLoaded', function() {
1593
1824
  // Apply i18n to all elements with data-i18n / data-i18n-placeholder
1594
1825
  applyI18n();
@@ -1602,6 +1833,20 @@
1602
1833
  try { ClawBenchNative.cancelSplash(); } catch(e) {}
1603
1834
  });
1604
1835
  }
1836
+ // Version-gate buttons. Wired here (not in the show hook) so they work
1837
+ // even if the gate is shown after the page has already loaded.
1838
+ var continueBtn = document.getElementById('gateContinue');
1839
+ if (continueBtn) {
1840
+ continueBtn.addEventListener('click', function() {
1841
+ try { ClawBenchNative.versionContinue(); } catch(e) {}
1842
+ });
1843
+ }
1844
+ var downloadBtn = document.getElementById('gateDownload');
1845
+ if (downloadBtn) {
1846
+ downloadBtn.addEventListener('click', function() {
1847
+ try { ClawBenchNative.versionDownload(); } catch(e) {}
1848
+ });
1849
+ }
1605
1850
  return;
1606
1851
  }
1607
1852
 
@@ -1655,64 +1900,81 @@
1655
1900
  }
1656
1901
  });
1657
1902
 
1658
- // Event: add and connect to new server
1903
+ // Event: save the add/edit form. Both "Add Server" and the row pencil land
1904
+ // here; the form's editUrl data attribute is what distinguishes them.
1905
+ // Saving never connects — the user picks the server and presses Connect.
1659
1906
  document.getElementById('addServerForm').addEventListener('submit', function(e) {
1660
1907
  e.preventDefault();
1661
1908
  if (connecting) return;
1662
1909
  hideError('addErrorMsg');
1663
1910
 
1664
- var host = document.getElementById('addHost').value.trim();
1665
- var port = document.getElementById('addPort').value.trim();
1911
+ var url = buildServerUrl(document.getElementById('addAddress').value);
1912
+ if (!url) {
1913
+ showError('addErrorMsg', t('enter_full_address'));
1914
+ document.getElementById('addAddress').focus();
1915
+ return;
1916
+ }
1666
1917
  var password = document.getElementById('addPassword').value;
1667
- var protocol = document.querySelector('input[name="addProtocol"]:checked').value;
1668
-
1669
- if (!host) {
1670
- showError('addErrorMsg', t('enter_address'));
1671
- document.getElementById('addHost').focus();
1918
+ var name = normalizeServerName(document.getElementById('addName').value);
1919
+ // The URL this form is editing ('' when adding). Captured before save so
1920
+ // the rename branch can retire the old entry afterwards.
1921
+ var editingUrl = document.getElementById('addServerForm').getAttribute('data-edit-url') || '';
1922
+
1923
+ // Renaming onto another saved server would silently overwrite that
1924
+ // server's password (saveServer keys on URL). Refuse instead.
1925
+ var clash = savedServers.some(function(s) {
1926
+ return s.url === url && s.url !== editingUrl;
1927
+ });
1928
+ if (clash) {
1929
+ showError('addErrorMsg', t('duplicate_server'));
1672
1930
  return;
1673
1931
  }
1674
- if (!port) {
1675
- port = (protocol === 'https') ? '443' : '80';
1932
+ // Names are unique labels. The edited entry is exempt from matching
1933
+ // itself; unnamed servers never collide with each other.
1934
+ if (findServerNameConflict(savedServers, name, editingUrl)) {
1935
+ showError('addErrorMsg', t('duplicate_name'));
1936
+ return;
1676
1937
  }
1677
1938
 
1678
- var url = protocol + '://' + host + ':' + port;
1679
- if (url.endsWith('/')) url = url.substring(0, url.length - 1);
1680
-
1681
- setConnecting(true, true);
1939
+ setSaving(true);
1940
+ // Both calls go through ipcRenderer.invoke, so failures surface as
1941
+ // rejections. Chain them: the rename must delete the old entry only after
1942
+ // the new one is written, and the list must reload after both settle.
1682
1943
  try {
1683
- // Save to native list first. See the note in the connect handler above:
1684
- // these are async, so the rejection must be handled explicitly.
1685
- ClawBenchNative.saveServer(url, password);
1686
- Promise.resolve(ClawBenchNative.connectToServer(url, password)).catch(function(err) {
1687
- showError('addErrorMsg', t('connection_failed') + (err && err.message ? err.message : ''));
1688
- setConnecting(false, true);
1689
- });
1944
+ Promise.resolve(ClawBenchNative.saveServerNamed(url, password, name))
1945
+ .then(function() {
1946
+ // Only when the address actually changed: deleting the same URL
1947
+ // would drop the entry we just saved.
1948
+ if (editingUrl && editingUrl !== url) {
1949
+ return Promise.resolve(ClawBenchNative.removeServer(editingUrl));
1950
+ }
1951
+ })
1952
+ .then(function() {
1953
+ setSaving(false);
1954
+ hideAddForm();
1955
+ })
1956
+ .catch(function(err) {
1957
+ showError('addErrorMsg', t('connection_failed') + (err && err.message ? err.message : ''));
1958
+ setSaving(false);
1959
+ });
1690
1960
  } catch(e) {
1691
1961
  showError('addErrorMsg', t('connection_failed') + e.message);
1692
- setConnecting(false, true);
1962
+ setSaving(false);
1693
1963
  }
1694
1964
  });
1695
1965
 
1696
- // Event: blur on the host field -> normalize a full URL into protocol + port.
1697
- // Blur, not paste/input: the field is only rewritten after the user leaves it,
1698
- // so typing is never disturbed mid-keystroke. Reading .value (rather than
1699
- // clipboard data) also sidesteps WebView clipboard restrictions.
1700
- document.getElementById('addHost').addEventListener('blur', function() {
1701
- var parsed = parseServerInput(document.getElementById('addHost').value);
1702
- // Not a URL we understand -> leave the field exactly as the user typed it.
1703
- if (!parsed) return;
1966
+ // Event: blur on the address field -> normalize a full address in place.
1967
+ // Blur, not paste/input: the field is only rewritten after the user leaves
1968
+ // it, so typing is never disturbed mid-keystroke. Reading .value (rather
1969
+ // than clipboard data) also sidesteps WebView clipboard restrictions.
1970
+ document.getElementById('addAddress').addEventListener('blur', function() {
1971
+ var url = buildServerUrl(document.getElementById('addAddress').value);
1972
+ // Incomplete address -> leave the field exactly as the user typed it; the
1973
+ // submit handler reports what is missing.
1974
+ if (!url) return;
1704
1975
 
1705
1976
  hideError('addErrorMsg');
1706
- document.getElementById('addHost').value = parsed.host;
1707
- // Only override the port when the URL carried one explicitly; the field
1708
- // keeps its default (20000) otherwise.
1709
- if (parsed.port !== null) {
1710
- document.getElementById('addPort').value = parsed.port;
1711
- }
1712
- // No scheme in the input -> leave the radio untouched.
1713
- if (parsed.protocol !== null) {
1714
- document.querySelector('input[name="addProtocol"][value="' + parsed.protocol + '"]').checked = true;
1715
- }
1977
+ document.getElementById('addAddress').value = url;
1716
1978
  });
1717
1979
 
1718
1980
  // Event: show add form
@@ -1768,11 +2030,19 @@
1768
2030
  html += ' <div class="server-item-info">';
1769
2031
  html += ' <span class="server-indicator"></span>';
1770
2032
  html += ' ' + SVG_SERVER;
1771
- html += ' <span class="server-item-url">' + escHtml(formatHost(srv.url)) + '</span>';
2033
+ // A named server shows its name; an unnamed one falls back to the
2034
+ // address. The full address always stays in the title tooltip, so a
2035
+ // named row is still identifiable (and copying it is possible).
2036
+ html += ' <span class="server-item-url" title="' + escHtml(srv.url) + '">' + escHtml(normalizeServerName(srv.name) || formatHost(srv.url)) + '</span>';
2037
+ html += ' </div>';
2038
+ html += ' <div class="server-item-actions">';
2039
+ html += ' <button class="server-edit-btn" onclick="event.stopPropagation(); editServer(\'' + escJs(srv.url) + '\')" title="' + t('edit_server') + '">';
2040
+ html += ' ' + SVG_PENCIL;
2041
+ html += ' </button>';
2042
+ html += ' <button class="server-delete-btn" onclick="event.stopPropagation(); deleteServer(\'' + escJs(srv.url) + '\')" title="' + t('delete') + '">';
2043
+ html += ' ' + SVG_X;
2044
+ html += ' </button>';
1772
2045
  html += ' </div>';
1773
- html += ' <button class="server-delete-btn" onclick="event.stopPropagation(); deleteServer(\'' + escJs(srv.url) + '\')" title="' + t('delete') + '">';
1774
- html += ' ' + SVG_X;
1775
- html += ' </button>';
1776
2046
  html += '</div>';
1777
2047
  });
1778
2048
 
@@ -1886,17 +2156,59 @@
1886
2156
  });
1887
2157
  }
1888
2158
 
2159
+ /**
2160
+ * Show the form for adding a NEW server: empty fields, "Add" button.
2161
+ */
1889
2162
  function showAddForm() {
2163
+ var form = document.getElementById('addServerForm');
2164
+ // Empty data-edit-url is what marks this as an add, not an edit.
2165
+ form.setAttribute('data-edit-url', '');
2166
+ document.getElementById('addFormTitle').style.display = 'none';
2167
+ document.getElementById('addName').value = '';
2168
+ document.getElementById('addAddress').value = '';
2169
+ document.getElementById('addPassword').value = '';
2170
+ document.getElementById('addBtnText').textContent = t('add');
2171
+
2172
+ document.getElementById('serverListSection').style.display = 'none';
2173
+ document.getElementById('connectForm').style.display = 'none';
2174
+ document.getElementById('addServerBtn').style.display = 'none';
2175
+ form.style.display = 'block';
2176
+ }
2177
+
2178
+ /**
2179
+ * Reuse the add form to edit an existing server. data-edit-url holds the
2180
+ * entry being edited so submit can tell the two modes apart and retire the
2181
+ * old URL when the address changes.
2182
+ */
2183
+ function editServer(url) {
2184
+ var srv = savedServers.find(function(s) { return s.url === url; });
2185
+ if (!srv) return;
2186
+
2187
+ var form = document.getElementById('addServerForm');
2188
+ form.setAttribute('data-edit-url', url);
2189
+ var title = document.getElementById('addFormTitle');
2190
+ title.textContent = t('edit_server');
2191
+ title.style.display = 'block';
2192
+ document.getElementById('addName').value = normalizeServerName(srv.name);
2193
+ document.getElementById('addAddress').value = srv.url;
2194
+ // Prefill the saved password so saving without retyping keeps it.
2195
+ document.getElementById('addPassword').value = srv.password || '';
2196
+ document.getElementById('addBtnText').textContent = t('save');
2197
+
1890
2198
  document.getElementById('serverListSection').style.display = 'none';
1891
2199
  document.getElementById('connectForm').style.display = 'none';
1892
2200
  document.getElementById('addServerBtn').style.display = 'none';
1893
- document.getElementById('addServerForm').style.display = 'block';
2201
+ form.style.display = 'block';
2202
+ hideError('addErrorMsg');
2203
+ document.getElementById('addAddress').focus();
1894
2204
  }
1895
2205
 
1896
2206
  function hideAddForm() {
1897
2207
  document.getElementById('addServerForm').style.display = 'none';
1898
2208
  document.getElementById('addErrorMsg').classList.remove('visible');
1899
- renderServerList();
2209
+ // Reload from native rather than re-rendering the stale in-memory array:
2210
+ // saving changed the list (new entry, promoted head, renamed URL).
2211
+ loadServerList();
1900
2212
  }
1901
2213
 
1902
2214
  function formatHost(url) {
@@ -1937,13 +2249,31 @@
1937
2249
  } else {
1938
2250
  btn.disabled = false;
1939
2251
  if (isAddForm) {
1940
- btn.innerHTML = '<span id="addBtnText">' + t('add_connect') + '</span>';
2252
+ // The form doubles as add and edit, so restore whichever label its
2253
+ // current mode calls for rather than always "Add".
2254
+ var editing = document.getElementById('addServerForm').getAttribute('data-edit-url');
2255
+ btn.innerHTML = '<span id="addBtnText">' + t(editing ? 'save' : 'add') + '</span>';
1941
2256
  } else {
1942
2257
  btn.innerHTML = '<span id="btnText">' + t('connect') + '</span>';
1943
2258
  }
1944
2259
  }
1945
2260
  }
1946
2261
 
2262
+ /**
2263
+ * Busy state for the add/edit form's own Save/Add button. Separate from
2264
+ * setConnecting because saving does not authenticate: showing "Authenticating"
2265
+ * would misdescribe what is happening.
2266
+ */
2267
+ function setSaving(saving) {
2268
+ var btn = document.getElementById('addConnectBtn');
2269
+ if (saving) {
2270
+ btn.disabled = true;
2271
+ btn.innerHTML = '<div class="spinner"></div><span>' + t('saving') + '</span>';
2272
+ } else {
2273
+ setConnecting(false, true);
2274
+ }
2275
+ }
2276
+
1947
2277
  // Called by native code on connection failure.
1948
2278
  // msg is the Chromium load-error description (e.g. "ERR_CONNECTION_REFUSED")
1949
2279
  // delivered from the main process's did-fail-load handler; empty when the
@@ -46,3 +46,61 @@ function parseServerInput(text) {
46
46
 
47
47
  return { protocol: protocol, host: host, port: port };
48
48
  }
49
+
50
+ /**
51
+ * Build the canonical server URL from a single free-text address.
52
+ *
53
+ * Strict on purpose: the login form no longer has a protocol radio or a port
54
+ * field, so the address must carry BOTH explicitly. A bare host or a
55
+ * scheme-less `host:port` returns null and the caller shows "enter a full
56
+ * address" rather than guessing a scheme/port the user never chose.
57
+ *
58
+ * Normalisation the parser already gives us: scheme lowercased, path/query/
59
+ * fragment dropped, surrounding whitespace trimmed.
60
+ *
61
+ * Returns 'scheme://host:port', or null when the input is not a complete
62
+ * server address. Callers must treat null as "reject, do not save".
63
+ */
64
+ function buildServerUrl(text) {
65
+ var parsed = parseServerInput(text);
66
+ if (!parsed || !parsed.protocol || !parsed.port) return null;
67
+ return parsed.protocol + '://' + parsed.host + ':' + parsed.port;
68
+ }
69
+
70
+ /**
71
+ * Normalise a server's display name: trimmed. Empty means "no name", which the
72
+ * list renders as the address instead.
73
+ *
74
+ * Returns '' for anything that is not a string, so callers can pass the raw
75
+ * field value without guarding.
76
+ */
77
+ function normalizeServerName(text) {
78
+ if (typeof text !== 'string') return '';
79
+ return text.trim();
80
+ }
81
+
82
+ /**
83
+ * Find the server whose NAME equals `name`, ignoring `exceptUrl`.
84
+ *
85
+ * Names are unique labels, so the form refuses to save a duplicate. The
86
+ * comparison is case-insensitive (users do not think of "Home" and "home" as
87
+ * different servers) and an empty `name` never matches — it means "no name",
88
+ * and unnamed servers must not collide with each other.
89
+ *
90
+ * `exceptUrl` is the entry currently being edited: a server must not collide
91
+ * with itself when its name is left unchanged.
92
+ *
93
+ * Returns the conflicting entry (an object with at least `.url`), or null.
94
+ */
95
+ function findServerNameConflict(servers, name, exceptUrl) {
96
+ var wanted = normalizeServerName(name).toLowerCase();
97
+ if (!wanted) return null;
98
+ if (!Array.isArray(servers)) return null;
99
+ for (var i = 0; i < servers.length; i++) {
100
+ var srv = servers[i];
101
+ if (!srv) continue;
102
+ if (exceptUrl && srv.url === exceptUrl) continue;
103
+ if (normalizeServerName(srv.name).toLowerCase() === wanted) return srv;
104
+ }
105
+ return null;
106
+ }