@xulongzhe/clawbench-desktop-linux-x64-payload 0.103.0 → 0.105.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.103.0",
3
+ "version": "0.105.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",
package/payload.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "electron": "44.4.3",
3
- "shell": "sha256-7a0d4e5e71bac0e161f65aa81fd51ec1800cb11901d85e9052e991ab903bbbb3"
3
+ "shell": "sha256-c6c6aeac1bb7aabd98de1d55a3127dff1c331139ae30f4dc7bfadc2e88caf73b"
4
4
  }
Binary file
@@ -29,7 +29,15 @@
29
29
  connection_failed: 'Connection failed: ',
30
30
  connect_error: 'Unable to connect. Check address and network.',
31
31
  wrong_password: 'Wrong password',
32
- too_many_attempts: 'Too many attempts'
32
+ too_many_attempts: 'Too many attempts',
33
+ // Splash overlay (?splash=1). Wording mirrors Android's splash_status_*
34
+ // strings so the two platforms read identically.
35
+ splash_connecting: 'Connecting…',
36
+ splash_loading: 'Loading page…',
37
+ splash_rendering: 'Rendering…',
38
+ splash_initializing: 'Initializing app…',
39
+ splash_cancel: 'Cancel connection',
40
+ window_close: 'Close'
33
41
  },
34
42
  zh: {
35
43
  slogan: '从终端到桌面',
@@ -53,7 +61,15 @@
53
61
  connection_failed: '连接失败: ',
54
62
  connect_error: '无法连接到服务器,请检查地址和网络。',
55
63
  wrong_password: '密码错误',
56
- too_many_attempts: '尝试次数过多'
64
+ too_many_attempts: '尝试次数过多',
65
+ // Splash overlay (?splash=1). Wording mirrors Android's splash_status_*
66
+ // strings so the two platforms read identically.
67
+ splash_connecting: '正在连接…',
68
+ splash_loading: '正在加载页面…',
69
+ splash_rendering: '正在渲染…',
70
+ splash_initializing: '正在初始化应用…',
71
+ splash_cancel: '取消连接',
72
+ window_close: '关闭'
57
73
  }
58
74
  };
59
75
  var LANG = 'en';
@@ -701,6 +717,76 @@
701
717
  position: relative;
702
718
  overflow: hidden;
703
719
  -webkit-tap-highlight-color: transparent;
720
+ /* This page IS the window on first run and whenever the server is
721
+ unreachable, and the window is frameless on Windows/Linux — so without a
722
+ drag region the login window could not be moved at all. The whole page is
723
+ the handle (there is no title bar), which is why every interactive
724
+ element below has to opt back out. Ignored on macOS/Android/browser. */
725
+ -webkit-app-region: drag;
726
+ -webkit-user-select: none;
727
+ user-select: none;
728
+ }
729
+
730
+ /* Opt out of the page-wide drag handle. Without this the inputs and buttons
731
+ would be undraggable-but-unclickable — a click would start a window move.
732
+ `.dlg-box` is included because the confirmation dialog's message text
733
+ should be selectable, and `.server-item` because its rows are clickable. */
734
+ input,
735
+ button,
736
+ select,
737
+ a,
738
+ .form-card,
739
+ .server-list,
740
+ .field-group,
741
+ .server-item,
742
+ .dlg-box,
743
+ .dlg-actions {
744
+ -webkit-app-region: no-drag;
745
+ }
746
+
747
+ /* Text the user may want to select (server URLs, error messages) opts out of
748
+ both the drag region and the inherited user-select:none. */
749
+ input,
750
+ .server-url,
751
+ .error-message,
752
+ .splash-stage {
753
+ -webkit-user-select: text;
754
+ user-select: text;
755
+ }
756
+
757
+ /* Window close, for the frameless Windows/Linux shell. Pinned to the window's
758
+ top-right — the same corner the native controls occupy on those platforms. */
759
+ .window-close {
760
+ position: fixed;
761
+ top: 0;
762
+ right: 0;
763
+ width: 46px;
764
+ height: 32px;
765
+ display: flex;
766
+ align-items: center;
767
+ justify-content: center;
768
+ padding: 0;
769
+ border: none;
770
+ border-radius: 0 0 0 6px;
771
+ background: transparent;
772
+ color: var(--text-hint);
773
+ cursor: pointer;
774
+ z-index: 200;
775
+ transition: background 150ms ease, color 150ms ease;
776
+ }
777
+
778
+ .window-close svg {
779
+ width: 15px;
780
+ height: 15px;
781
+ }
782
+
783
+ .window-close:hover {
784
+ background: var(--color-red, #ef4444);
785
+ color: #fff;
786
+ }
787
+
788
+ .window-close[hidden] {
789
+ display: none;
704
790
  }
705
791
 
706
792
  /* Decorative background — matches LoginView.vue */
@@ -1272,12 +1358,138 @@
1272
1358
  padding-bottom: env(safe-area-inset-bottom);
1273
1359
  }
1274
1360
  }
1361
+ /* ── Splash overlay (?splash=1) ──────────────────────────────────────────
1362
+ Shown by the main process while connecting to a server and booting the
1363
+ app. Mirrors Android's native splash: a circular logo with a rotating
1364
+ sweep ring, a stage line, and a cancel button. The ring reproduces
1365
+ android/app/src/main/res/drawable/splash_sweep.xml (a dim full ring plus a
1366
+ ~90° bright arc spun linearly over 1.6s). */
1367
+ body.splash-mode .content { display: none; }
1368
+
1369
+ .splash {
1370
+ position: fixed;
1371
+ inset: 0;
1372
+ display: none;
1373
+ flex-direction: column;
1374
+ align-items: center;
1375
+ justify-content: center;
1376
+ background: var(--bg-primary);
1377
+ z-index: 100;
1378
+ }
1379
+ body.splash-mode .splash { display: flex; }
1380
+
1381
+ /* Fade-out hook, called by the main process just before it hides the view. */
1382
+ .splash.is-fading { opacity: 0; transition: opacity 200ms ease; }
1383
+
1384
+ .splash-logo-wrapper {
1385
+ position: relative;
1386
+ width: 132px;
1387
+ height: 132px;
1388
+ }
1389
+
1390
+ /* Sweep ring: a dim circle with one bright arc, spun by rotating the box. */
1391
+ .splash-sweep {
1392
+ position: absolute;
1393
+ inset: 0;
1394
+ border-radius: 50%;
1395
+ border: 2px solid var(--border-color);
1396
+ opacity: 0.4;
1397
+ animation: splash-spin 1.6s linear infinite;
1398
+ }
1399
+ .splash-sweep::after {
1400
+ content: '';
1401
+ position: absolute;
1402
+ inset: -2px;
1403
+ border-radius: 50%;
1404
+ border: 3px solid transparent;
1405
+ border-top-color: var(--accent-color);
1406
+ border-right-color: var(--accent-color);
1407
+ /* Two adjacent edges approximate the native 90° sweep segment without an
1408
+ SVG, so no extra asset ships for this. */
1409
+ transform: rotate(45deg);
1410
+ }
1411
+
1412
+ @keyframes splash-spin {
1413
+ from { transform: rotate(0deg); }
1414
+ to { transform: rotate(360deg); }
1415
+ }
1416
+
1417
+ .splash-logo {
1418
+ position: absolute;
1419
+ top: 12px;
1420
+ left: 12px;
1421
+ width: 108px;
1422
+ height: 108px;
1423
+ border-radius: 50%;
1424
+ object-fit: cover;
1425
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1426
+ }
1427
+
1428
+ .splash-title {
1429
+ margin-top: 24px;
1430
+ font-size: 20px;
1431
+ font-weight: 500;
1432
+ color: var(--text-primary);
1433
+ letter-spacing: 0.06em;
1434
+ opacity: 0.85;
1435
+ }
1436
+
1437
+ .splash-stage {
1438
+ margin-top: 12px;
1439
+ font-size: 14px;
1440
+ font-weight: 500;
1441
+ color: var(--text-muted);
1442
+ opacity: 0.7;
1443
+ }
1444
+
1445
+ .splash-cancel {
1446
+ margin-top: 28px;
1447
+ padding: 8px 16px;
1448
+ font-size: 13px;
1449
+ font-family: inherit;
1450
+ color: var(--text-hint);
1451
+ background: transparent;
1452
+ border: 1px solid var(--border-color);
1453
+ border-radius: 6px;
1454
+ cursor: pointer;
1455
+ transition: background 150ms ease, color 150ms ease;
1456
+ }
1457
+ .splash-cancel:hover {
1458
+ background: var(--bg-tertiary);
1459
+ color: var(--text-primary);
1460
+ }
1275
1461
  </style>
1276
1462
  </head>
1277
1463
  <body>
1464
+ <!-- Splash overlay (?splash=1). Rendered by the main process as a native
1465
+ WebContentsView floating above the app while it connects and boots; see
1466
+ desktop/src/main/splash.ts. Hidden unless the query flag is present. -->
1467
+ <div class="splash" id="splash">
1468
+ <div class="splash-logo-wrapper">
1469
+ <div class="splash-sweep"></div>
1470
+ <img class="splash-logo" src="logo.png" alt="ClawBench">
1471
+ </div>
1472
+ <div class="splash-title">ClawBench</div>
1473
+ <div class="splash-stage" id="splashStage" data-i18n="splash_connecting">正在连接…</div>
1474
+ <button type="button" class="splash-cancel" id="splashCancel" data-i18n="splash_cancel">取消连接</button>
1475
+ </div>
1476
+
1278
1477
  <div class="bg-gradient"></div>
1279
1478
  <div class="bg-grid"></div>
1280
1479
 
1480
+ <!-- Window close, for the frameless Windows/Linux shell.
1481
+ This document has no AppHeader, so it is the ONLY way to close the window
1482
+ on first run or when the server is unreachable — without it the window
1483
+ would have no close affordance at all. Hidden unless the host reports
1484
+ app-drawn controls (macOS keeps its traffic lights, and Android is not a
1485
+ window), so it never duplicates a native button. -->
1486
+ <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>
1487
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1488
+ <line x1="6" y1="6" x2="18" y2="18"/>
1489
+ <line x1="18" y1="6" x2="6" y2="18"/>
1490
+ </svg>
1491
+ </button>
1492
+
1281
1493
  <div class="content">
1282
1494
  <!-- Brand -->
1283
1495
  <div class="brand">
@@ -1402,6 +1614,7 @@
1402
1614
  </div>
1403
1615
  </div>
1404
1616
 
1617
+ <script src="url-utils.js"></script>
1405
1618
  <script>
1406
1619
  var connecting = false;
1407
1620
  var savedServers = [];
@@ -1417,12 +1630,94 @@
1417
1630
  document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
1418
1631
  el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
1419
1632
  });
1633
+ // Attribute-valued strings (tooltips, accessible names). The icon-only
1634
+ // close button has no text to translate, so without these it would keep its
1635
+ // hard-coded English label in every language.
1636
+ document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
1637
+ el.title = t(el.getAttribute('data-i18n-title'));
1638
+ });
1639
+ document.querySelectorAll('[data-i18n-aria-label]').forEach(function(el) {
1640
+ el.setAttribute('aria-label', t(el.getAttribute('data-i18n-aria-label')));
1641
+ });
1420
1642
  }
1421
1643
 
1644
+ // ── Splash overlay (?splash=1) ──────────────────────────────────────────
1645
+ // The main process loads this same page with ?splash=1 as a native view
1646
+ // floating over the app while it connects and boots. In that mode the login
1647
+ // form is irrelevant: only the splash markup is shown, and the process drives
1648
+ // the stage text through __splashSetStage.
1649
+ var SPLASH_MODE = /(?:^|[?&])splash=1(?:&|$)/.test(window.location.search);
1650
+ var SPLASH_STAGE_KEYS = {
1651
+ connecting: 'splash_connecting',
1652
+ loading: 'splash_loading',
1653
+ rendering: 'splash_rendering',
1654
+ initializing: 'splash_initializing'
1655
+ };
1656
+
1657
+ /** Advance the splash stage text. Called from the main process. */
1658
+ window.__splashSetStage = function(stage) {
1659
+ var key = SPLASH_STAGE_KEYS[stage];
1660
+ if (!key) return;
1661
+ var el = document.getElementById('splashStage');
1662
+ if (el) el.textContent = t(key);
1663
+ };
1664
+
1665
+ /** Begin the splash fade-out. The main process hides the view shortly after. */
1666
+ window.__splashFadeOut = function() {
1667
+ var el = document.getElementById('splash');
1668
+ if (el) el.classList.add('is-fading');
1669
+ };
1670
+
1671
+ /**
1672
+ * Reset the overlay for a fresh show. The page is REUSED across connects
1673
+ * (the main process only loads it once), so the fade class left by the
1674
+ * previous dismissal must be cleared — otherwise the second connect renders
1675
+ * the overlay fully transparent.
1676
+ */
1677
+ window.__splashReset = function() {
1678
+ var el = document.getElementById('splash');
1679
+ if (el) el.classList.remove('is-fading');
1680
+ };
1681
+
1682
+ if (SPLASH_MODE) {
1683
+ document.body.classList.add('splash-mode');
1684
+ }
1685
+
1686
+ /**
1687
+ * Reveal and wire the window close button when the host draws its own
1688
+ * controls (frameless Windows/Linux). `hasCustomWindowControls` is absent on
1689
+ * older hosts, macOS and Android — the button stays hidden there, so it never
1690
+ * duplicates a native close button. Runs before DOMContentLoaded so the
1691
+ * button is usable even while the splash overlay is up.
1692
+ */
1693
+ (function initWindowClose() {
1694
+ var btn = document.getElementById('windowClose');
1695
+ if (!btn) return;
1696
+ var supported = false;
1697
+ try { supported = ClawBenchNative.hasCustomWindowControls && ClawBenchNative.hasCustomWindowControls() === true; } catch(e) {}
1698
+ if (!supported) return;
1699
+ btn.hidden = false;
1700
+ btn.addEventListener('click', function() {
1701
+ try { ClawBenchNative.windowClose(); } catch(e) {}
1702
+ });
1703
+ })();
1704
+
1422
1705
  window.addEventListener('DOMContentLoaded', function() {
1423
1706
  // Apply i18n to all elements with data-i18n / data-i18n-placeholder
1424
1707
  applyI18n();
1425
1708
 
1709
+ if (SPLASH_MODE) {
1710
+ // Splash mode is display-only apart from the cancel button; skip the
1711
+ // login form's data loading entirely.
1712
+ var cancelBtn = document.getElementById('splashCancel');
1713
+ if (cancelBtn) {
1714
+ cancelBtn.addEventListener('click', function() {
1715
+ try { ClawBenchNative.cancelSplash(); } catch(e) {}
1716
+ });
1717
+ }
1718
+ return;
1719
+ }
1720
+
1426
1721
  // Load server list (single source of truth)
1427
1722
  loadServerList();
1428
1723
 
@@ -1511,6 +1806,28 @@
1511
1806
  }
1512
1807
  });
1513
1808
 
1809
+ // Event: blur on the host field -> normalize a full URL into protocol + port.
1810
+ // Blur, not paste/input: the field is only rewritten after the user leaves it,
1811
+ // so typing is never disturbed mid-keystroke. Reading .value (rather than
1812
+ // clipboard data) also sidesteps WebView clipboard restrictions.
1813
+ document.getElementById('addHost').addEventListener('blur', function() {
1814
+ var parsed = parseServerInput(document.getElementById('addHost').value);
1815
+ // Not a URL we understand -> leave the field exactly as the user typed it.
1816
+ if (!parsed) return;
1817
+
1818
+ hideError('addErrorMsg');
1819
+ document.getElementById('addHost').value = parsed.host;
1820
+ // Only override the port when the URL carried one explicitly; the field
1821
+ // keeps its default (20000) otherwise.
1822
+ if (parsed.port !== null) {
1823
+ document.getElementById('addPort').value = parsed.port;
1824
+ }
1825
+ // No scheme in the input -> leave the radio untouched.
1826
+ if (parsed.protocol !== null) {
1827
+ document.querySelector('input[name="addProtocol"][value="' + parsed.protocol + '"]').checked = true;
1828
+ }
1829
+ });
1830
+
1514
1831
  // Event: show add form
1515
1832
  document.getElementById('addServerBtn').addEventListener('click', function() {
1516
1833
  showAddForm();
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Parse a server address into protocol / host / port.
3
+ *
4
+ * Pure and dependency-free on purpose: it must be evaluable in a bare sandbox
5
+ * (no DOM, no ClawBenchNative). Deliberately NOT built on `new URL()`:
6
+ * new URL('192.168.1.100:8080') -> throws Invalid URL
7
+ * new URL('example.com:8080') -> protocol "example.com:", hostname ""
8
+ * so the scheme-less form users actually type cannot go through it. A single
9
+ * regex treats both "with scheme" and "without scheme" uniformly.
10
+ *
11
+ * Returns { protocol: 'http'|'https'|null, host: string, port: string|null },
12
+ * or null when the input is not a server address we understand. Callers must
13
+ * treat null as "leave the field alone".
14
+ */
15
+ function parseServerInput(text) {
16
+ if (typeof text !== 'string') return null;
17
+ var s = text.trim();
18
+ if (!s) return null;
19
+
20
+ var protocol = null;
21
+ var rest = s;
22
+
23
+ // Optional scheme. Anything other than http/https is not ours to interpret.
24
+ var schemeMatch = rest.match(/^([A-Za-z][A-Za-z0-9+.-]*):\/\//);
25
+ if (schemeMatch) {
26
+ var scheme = schemeMatch[1].toLowerCase();
27
+ if (scheme !== 'http' && scheme !== 'https') return null;
28
+ protocol = scheme;
29
+ rest = rest.slice(schemeMatch[0].length);
30
+ }
31
+
32
+ // Drop path, query and fragment — only scheme/host/port are meaningful here.
33
+ rest = rest.split(/[/?#]/)[0];
34
+
35
+ var host = rest;
36
+ var port = null;
37
+ var portMatch = rest.match(/^(.*):(\d+)$/);
38
+ if (portMatch) {
39
+ host = portMatch[1];
40
+ port = portMatch[2];
41
+ }
42
+
43
+ // Host must look like a hostname or IPv4 literal. This is also what rejects
44
+ // 'not a url', 'http://' (empty host) and 'user:pass@host' (contains '@').
45
+ if (!/^[A-Za-z0-9._-]+$/.test(host)) return null;
46
+
47
+ return { protocol: protocol, host: host, port: port };
48
+ }