forge-jsxy 1.0.77 → 1.0.79

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.
@@ -1,4 +1,4 @@
1
- \<!DOCTYPE html>
1
+ <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -1608,13 +1608,13 @@
1608
1608
  </div>
1609
1609
  <div id="bar-row-nav" class="fe-bar-row" role="toolbar" aria-label="Browse and search">
1610
1610
  <span id="fe-build" class="fe-build-pill fe-toggle-extra" title="Forge-jsxy build stamp — Ctrl+Shift+R if UI looks outdated.">2026.06i</span>
1611
- <button type="button" class="sec fe-icon-btn" id="btn-hist-back" onclick="goHistBack()" title="History back; at C:\\ / drive root also opens drive list" aria-label="Back"><span class="codicon codicon-chevron-left" aria-hidden="true"></span></button>
1611
+ <button type="button" class="sec fe-icon-btn" id="btn-hist-back" onclick="goHistBack()" title="History back; when already at drive root (Windows or /), opens the drive list" aria-label="Back"><span class="codicon codicon-chevron-left" aria-hidden="true"></span></button>
1612
1612
  <button type="button" class="sec fe-icon-btn" id="btn-hist-fwd" onclick="goHistForward()" title="Next folder in history" aria-label="Forward"><span class="codicon codicon-chevron-right" aria-hidden="true"></span></button>
1613
1613
  <button type="button" class="sec fe-icon-btn" onclick="goUp()" title="Parent folder or drive list" aria-label="Up"><span class="codicon codicon-arrow-up" aria-hidden="true"></span></button>
1614
1614
  <button type="button" class="sec fe-icon-btn" onclick="refresh()" title="Reload current folder listing" aria-label="Refresh"><span class="codicon codicon-refresh" aria-hidden="true"></span></button>
1615
1615
  <input id="path" placeholder="Current folder path" title="Shows the open folder path (Windows Explorer–style). Type a path and press Go."/>
1616
1616
  <button type="button" class="sec" onclick="goPath()">Go</button>
1617
- <input id="search" placeholder="Search tree (e.g. *.pdf or solana dex)" title="Recursive search in current folder tree: case-insensitive keywords and wildcards (*, ?)."/>
1617
+ <input id="search" placeholder="Search this folder tree keywords, *.pdf, ?.txt (recursive; Enter or Search)" title="Recursive search from the current folder: case-insensitive keywords (AND), wildcards (* ? [] {}). Press Enter or click Search. Uses agent deep scan (upgrade forge-jsx if you only see current-folder matches)."/>
1618
1618
  <button type="button" class="sec" id="btn-search" onclick="runSearch()">Search</button>
1619
1619
  <button type="button" class="sec" id="btn-search-clear" onclick="clearSearch()">Clear search</button>
1620
1620
  <button type="button" class="sec fe-toggle-extra" onclick="viewSel()">View</button>
@@ -1702,6 +1702,24 @@ let _explorerAutoConnectTimer = null;
1702
1702
  let _explorerVoluntaryDisconnect = false;
1703
1703
  /** From agent `system_info` after `get_info` — shell one-liners + screenshot button (win32 / linux / darwin). */
1704
1704
  let agentPlatform = '';
1705
+ /** Semver from agent `system_info` — skips WebRTC offers when known-old vs this relay bundle. */
1706
+ let sessionForgeJsxVersion = '';
1707
+ /** Relay-advertised WebRTC (signaling stays on WebSocket; optional `forge-rc` P2P for small fs/rc JSON). */
1708
+ let relayWebrtcSignaling = false;
1709
+ let relayRtcIceServers = null;
1710
+ let forgeRtcPc = null;
1711
+ let forgeRtcDc = null;
1712
+ /** Ordered bulk binary channel for large `fs_read`/`fs_zip` (JSON hdr + raw bytes). */
1713
+ let forgeRtcDcBulk = null;
1714
+ let forgeBulkFeExpectHdr = true;
1715
+ let forgeBulkFeRx = null;
1716
+ let forgeRtcRemoteDescDone = false;
1717
+ const forgeRtcPendingRemoteCandidates = [];
1718
+ let forgeRtcProbeStarted = false;
1719
+ let forgeRtcReconnectTimer = null;
1720
+ let forgeRtcReconnectAttempts = 0;
1721
+ const FORGE_RTC_MAX_RECONNECT = 2;
1722
+ var FORGE_AGENT_WEBRTC_MIN_VERSION = '__FORGE_AGENT_WEBRTC_MIN_VERSION__';
1705
1723
  let wantScreenshotRid = null;
1706
1724
  let screenshotBlobUrl = null;
1707
1725
  /** Resizable explorer panes (`#fe-splitter-v` / `#fe-splitter-h`) — sizes persisted in localStorage. */
@@ -1762,6 +1780,287 @@ function scheduleFsChunkRequest(fn){
1762
1780
  if(WS_CHUNK_REQUEST_GAP_MS > 0) setTimeout(fn, WS_CHUNK_REQUEST_GAP_MS);
1763
1781
  else fn();
1764
1782
  }
1783
+ function parseVersionFe(v){
1784
+ return String(v || '').split('.').map(function(n){ return parseInt(n, 10); }).filter(function(n){ return isFinite(n); });
1785
+ }
1786
+ function versionLtFe(a, b){
1787
+ var av = parseVersionFe(a), bv = parseVersionFe(b);
1788
+ var n = Math.max(av.length, bv.length);
1789
+ for (var i = 0; i < n; i++) {
1790
+ var ai = av[i] || 0, bi = bv[i] || 0;
1791
+ if (ai < bi) return true;
1792
+ if (ai > bi) return false;
1793
+ }
1794
+ return false;
1795
+ }
1796
+ function resetForgeBulkFeInbound(){
1797
+ forgeBulkFeExpectHdr = true;
1798
+ forgeBulkFeRx = null;
1799
+ }
1800
+ var FORGE_BULK_MAX_BODY_BYTES_FE = 96468992;
1801
+ var FORGE_BULK_V2_MAX_CHUNK_ADV_FE = 262144;
1802
+ /** Must match `FORGE_BULK_V2_MIN_CHUNK_SZ` in forgeBulkDc.ts. */
1803
+ var FORGE_BULK_V2_MIN_CHUNK_ADV_FE = 1024;
1804
+ function forgeBulkFeBytesToB64(u8){
1805
+ var bin = '';
1806
+ var CH = 0x8000;
1807
+ for (var i = 0; i < u8.length; i += CH) {
1808
+ bin += String.fromCharCode.apply(null, u8.subarray(i, Math.min(i + CH, u8.length)));
1809
+ }
1810
+ return btoa(bin);
1811
+ }
1812
+ function forgeBulkFeStripHdr(hdr){
1813
+ var msg = {};
1814
+ for (var k in hdr) {
1815
+ if (
1816
+ Object.prototype.hasOwnProperty.call(hdr, k) &&
1817
+ k !== '_fb' &&
1818
+ k !== 'v' &&
1819
+ k !== 'byte_len' &&
1820
+ k !== 'chunk_sz'
1821
+ ) {
1822
+ msg[k] = hdr[k];
1823
+ }
1824
+ }
1825
+ return msg;
1826
+ }
1827
+ function attachForgeRtcDcBulkFe(pc){
1828
+ resetForgeBulkFeInbound();
1829
+ forgeRtcDcBulk = null;
1830
+ try {
1831
+ forgeRtcDcBulk = pc.createDataChannel('forge-bulk', { ordered: true });
1832
+ } catch (eBk) {
1833
+ return;
1834
+ }
1835
+ forgeRtcDcBulk.binaryType = 'arraybuffer';
1836
+ forgeRtcDcBulk.onmessage = function(ev){
1837
+ try {
1838
+ if (!forgeBulkFeExpectHdr && typeof ev.data === 'string') {
1839
+ resetForgeBulkFeInbound();
1840
+ }
1841
+ if (forgeBulkFeExpectHdr) {
1842
+ if (typeof ev.data !== 'string') {
1843
+ resetForgeBulkFeInbound();
1844
+ return;
1845
+ }
1846
+ var j = JSON.parse(ev.data);
1847
+ if (j && j._fb === 'abort') {
1848
+ resetForgeBulkFeInbound();
1849
+ return;
1850
+ }
1851
+ if (!j || j._fb !== 'hdr') {
1852
+ resetForgeBulkFeInbound();
1853
+ return;
1854
+ }
1855
+ var ver = Number(j.v);
1856
+ if (ver !== 1 && ver !== 2) {
1857
+ resetForgeBulkFeInbound();
1858
+ return;
1859
+ }
1860
+ var bl = Number(j.byte_len);
1861
+ if (!isFinite(bl) || bl < 0 || bl > FORGE_BULK_MAX_BODY_BYTES_FE || Math.floor(bl) !== bl) {
1862
+ resetForgeBulkFeInbound();
1863
+ return;
1864
+ }
1865
+ bl = bl | 0;
1866
+ if (bl === 0) {
1867
+ var msg0 = forgeBulkFeStripHdr(j);
1868
+ msg0.b64 = '';
1869
+ resetForgeBulkFeInbound();
1870
+ onMsg(msg0);
1871
+ return;
1872
+ }
1873
+ if (ver === 1) {
1874
+ forgeBulkFeRx = { phase: 'v1', hdr: j };
1875
+ forgeBulkFeExpectHdr = false;
1876
+ return;
1877
+ }
1878
+ var cs = Number(j.chunk_sz);
1879
+ if (!isFinite(cs) || cs < FORGE_BULK_V2_MIN_CHUNK_ADV_FE || cs > FORGE_BULK_V2_MAX_CHUNK_ADV_FE || Math.floor(cs) !== cs) {
1880
+ resetForgeBulkFeInbound();
1881
+ return;
1882
+ }
1883
+ cs = cs | 0;
1884
+ var buf;
1885
+ try {
1886
+ buf = new Uint8Array(bl);
1887
+ } catch (eAlloc) {
1888
+ resetForgeBulkFeInbound();
1889
+ return;
1890
+ }
1891
+ forgeBulkFeRx = { phase: 'v2', hdr: j, buf: buf, filled: 0, chunkSz: cs };
1892
+ forgeBulkFeExpectHdr = false;
1893
+ return;
1894
+ }
1895
+
1896
+ var u8 = ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new Uint8Array();
1897
+ var rx = forgeBulkFeRx;
1898
+ if (!rx) {
1899
+ resetForgeBulkFeInbound();
1900
+ return;
1901
+ }
1902
+
1903
+ if (rx.phase === 'v1') {
1904
+ var hdr1 = rx.hdr;
1905
+ var bl1 = Number(hdr1.byte_len) | 0;
1906
+ if (u8.length !== bl1) {
1907
+ resetForgeBulkFeInbound();
1908
+ return;
1909
+ }
1910
+ forgeBulkFeRx = null;
1911
+ forgeBulkFeExpectHdr = true;
1912
+ var msg1 = forgeBulkFeStripHdr(hdr1);
1913
+ msg1.b64 = forgeBulkFeBytesToB64(u8);
1914
+ onMsg(msg1);
1915
+ return;
1916
+ }
1917
+
1918
+ if (rx.phase === 'v2') {
1919
+ var rem = (Number(rx.hdr.byte_len) | 0) - rx.filled;
1920
+ if (u8.length <= 0 || u8.length > rem) {
1921
+ resetForgeBulkFeInbound();
1922
+ return;
1923
+ }
1924
+ if (rem > rx.chunkSz) {
1925
+ if (u8.length !== rx.chunkSz) {
1926
+ resetForgeBulkFeInbound();
1927
+ return;
1928
+ }
1929
+ } else if (u8.length !== rem) {
1930
+ resetForgeBulkFeInbound();
1931
+ return;
1932
+ }
1933
+ rx.buf.set(u8, rx.filled);
1934
+ rx.filled += u8.length;
1935
+ if (rx.filled === (Number(rx.hdr.byte_len) | 0)) {
1936
+ var msg2 = forgeBulkFeStripHdr(rx.hdr);
1937
+ msg2.b64 = forgeBulkFeBytesToB64(rx.buf);
1938
+ resetForgeBulkFeInbound();
1939
+ onMsg(msg2);
1940
+ }
1941
+ return;
1942
+ }
1943
+
1944
+ resetForgeBulkFeInbound();
1945
+ } catch (eBkMsg) {
1946
+ resetForgeBulkFeInbound();
1947
+ }
1948
+ };
1949
+ }
1950
+ async function flushForgeRtcRemoteCandidatesFe(){
1951
+ var pc = forgeRtcPc;
1952
+ if (!pc) return;
1953
+ var pending = forgeRtcPendingRemoteCandidates.splice(0, forgeRtcPendingRemoteCandidates.length);
1954
+ for (var i = 0; i < pending.length; i++) {
1955
+ try {
1956
+ await pc.addIceCandidate(pending[i]);
1957
+ } catch (e) {}
1958
+ }
1959
+ }
1960
+ function teardownForgeRtcExplorer(){
1961
+ forgeRtcRemoteDescDone = false;
1962
+ forgeRtcPendingRemoteCandidates.length = 0;
1963
+ forgeRtcDc = null;
1964
+ forgeRtcDcBulk = null;
1965
+ resetForgeBulkFeInbound();
1966
+ try {
1967
+ if (forgeRtcPc) {
1968
+ forgeRtcPc.close();
1969
+ forgeRtcPc = null;
1970
+ }
1971
+ } catch (e) {}
1972
+ forgeRtcProbeStarted = false;
1973
+ }
1974
+ function scheduleForgeRtcReconnectExplorer(){
1975
+ if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
1976
+ if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
1977
+ if (forgeRtcReconnectTimer) return;
1978
+ var delayMs = 2400 + forgeRtcReconnectAttempts * 800;
1979
+ forgeRtcReconnectTimer = setTimeout(function(){
1980
+ forgeRtcReconnectTimer = null;
1981
+ if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
1982
+ if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
1983
+ forgeRtcReconnectAttempts++;
1984
+ if (forgeRtcProbeStarted) return;
1985
+ tryForgeRtcExplorerProbe();
1986
+ }, delayMs);
1987
+ }
1988
+ /** Same labels/agent behavior as remote control; uploads & huge reads stay on WebSocket (SCTP-safe size cap in `send`). */
1989
+ function tryForgeRtcExplorerProbe(){
1990
+ if (forgeRtcProbeStarted || !relayWebrtcSignaling) return;
1991
+ if (typeof RTCPeerConnection === 'undefined') return;
1992
+ if (!ws || ws.readyState !== 1 || !authed) return;
1993
+ var minV = String(typeof FORGE_AGENT_WEBRTC_MIN_VERSION !== 'undefined' ? FORGE_AGENT_WEBRTC_MIN_VERSION : '').trim();
1994
+ if (/^\d/.test(minV) && sessionForgeJsxVersion && versionLtFe(sessionForgeJsxVersion, minV)) return;
1995
+ forgeRtcProbeStarted = true;
1996
+ forgeRtcRemoteDescDone = false;
1997
+ forgeRtcPendingRemoteCandidates.length = 0;
1998
+ forgeRtcDc = null;
1999
+ forgeRtcDcBulk = null;
2000
+ resetForgeBulkFeInbound();
2001
+ var ice = Array.isArray(relayRtcIceServers) && relayRtcIceServers.length > 0
2002
+ ? relayRtcIceServers
2003
+ : [{ urls: 'stun:stun.l.google.com:19302' }];
2004
+ try {
2005
+ try {
2006
+ forgeRtcPc = new RTCPeerConnection({
2007
+ iceServers: ice,
2008
+ iceTransportPolicy: 'all',
2009
+ bundlePolicy: 'max-bundle',
2010
+ rtcpMuxPolicy: 'require',
2011
+ iceCandidatePoolSize: 10,
2012
+ });
2013
+ } catch (ePc) {
2014
+ try {
2015
+ forgeRtcPc = new RTCPeerConnection({
2016
+ iceServers: ice,
2017
+ iceTransportPolicy: 'all',
2018
+ bundlePolicy: 'max-bundle',
2019
+ rtcpMuxPolicy: 'require',
2020
+ });
2021
+ } catch (ePc2) {
2022
+ forgeRtcPc = new RTCPeerConnection({ iceServers: ice });
2023
+ }
2024
+ }
2025
+ forgeRtcDc = forgeRtcPc.createDataChannel('forge-rc', { ordered: false });
2026
+ forgeRtcDc.onmessage = function(ev){
2027
+ try {
2028
+ var parsed = JSON.parse(String(ev.data || ''));
2029
+ onMsg(parsed);
2030
+ } catch (e1) {}
2031
+ };
2032
+ attachForgeRtcDcBulkFe(forgeRtcPc);
2033
+ forgeRtcPc.onconnectionstatechange = function(){
2034
+ try {
2035
+ var st = forgeRtcPc && forgeRtcPc.connectionState;
2036
+ if (st === 'failed') {
2037
+ teardownForgeRtcExplorer();
2038
+ scheduleForgeRtcReconnectExplorer();
2039
+ }
2040
+ } catch (eCs) {}
2041
+ };
2042
+ forgeRtcPc.onicecandidate = function(ev){
2043
+ if (!ws || ws.readyState !== 1) return;
2044
+ if (ev && ev.candidate) {
2045
+ try {
2046
+ ws.send(JSON.stringify({
2047
+ type: 'forge_rtc_candidate',
2048
+ candidate: ev.candidate.candidate,
2049
+ sdpMid: ev.candidate.sdpMid,
2050
+ sdpMLineIndex: ev.candidate.sdpMLineIndex,
2051
+ }));
2052
+ } catch (e2) {}
2053
+ }
2054
+ };
2055
+ forgeRtcPc.createOffer().then(function(offer){
2056
+ return forgeRtcPc.setLocalDescription(offer).then(function(){
2057
+ ws.send(JSON.stringify({ type: 'forge_rtc_offer', sdp: offer.sdp, sdpType: offer.type }));
2058
+ });
2059
+ }).catch(function(){ teardownForgeRtcExplorer(); });
2060
+ } catch (e) {
2061
+ teardownForgeRtcExplorer();
2062
+ }
2063
+ }
1765
2064
  let _agentHintTimer = null;
1766
2065
  function clearAgentHintTimer(){
1767
2066
  if(_agentHintTimer){ clearTimeout(_agentHintTimer); _agentHintTimer = null; }
@@ -4018,6 +4317,14 @@ async function doConnect(){
4018
4317
  _explorerAutoConnectTimer = null;
4019
4318
  }
4020
4319
  _explorerVoluntaryDisconnect = false;
4320
+ if(forgeRtcReconnectTimer){
4321
+ clearTimeout(forgeRtcReconnectTimer);
4322
+ forgeRtcReconnectTimer = null;
4323
+ }
4324
+ forgeRtcReconnectAttempts = 0;
4325
+ teardownForgeRtcExplorer();
4326
+ relayWebrtcSignaling = false;
4327
+ relayRtcIceServers = null;
4021
4328
  if(ws){
4022
4329
  try{
4023
4330
  ws.onopen = null;
@@ -4110,17 +4417,56 @@ async function doConnect(){
4110
4417
  };
4111
4418
  activeSock.onmessage = function(ev){
4112
4419
  if(ws !== activeSock) return;
4420
+ function dispatchParsed(msg){
4421
+ var mt = String(msg && msg.type || '');
4422
+ if(mt === 'forge_rtc_answer'){
4423
+ if(!forgeRtcPc) return;
4424
+ var sdp = String(msg.sdp || '');
4425
+ var typ = String(msg.sdpType || 'answer');
4426
+ Promise.resolve().then(function(){
4427
+ return forgeRtcPc.setRemoteDescription({ type: typ, sdp: sdp }).then(function(){
4428
+ forgeRtcRemoteDescDone = true;
4429
+ return flushForgeRtcRemoteCandidatesFe();
4430
+ });
4431
+ }).catch(function(){});
4432
+ return;
4433
+ }
4434
+ if(mt === 'forge_rtc_agent_candidate'){
4435
+ if(!forgeRtcPc) return;
4436
+ var cand = String(msg.candidate || '').trim();
4437
+ if(!cand) return;
4438
+ var cinit = {
4439
+ candidate: cand,
4440
+ sdpMid: msg.sdpMid != null ? String(msg.sdpMid) : null,
4441
+ sdpMLineIndex: isFinite(Number(msg.sdpMLineIndex)) ? Number(msg.sdpMLineIndex) : null,
4442
+ };
4443
+ Promise.resolve().then(function(){
4444
+ if(!forgeRtcRemoteDescDone){
4445
+ forgeRtcPendingRemoteCandidates.push(cinit);
4446
+ } else {
4447
+ return forgeRtcPc.addIceCandidate(cinit);
4448
+ }
4449
+ }).catch(function(){});
4450
+ return;
4451
+ }
4452
+ onMsg(msg);
4453
+ }
4113
4454
  if(ev.data instanceof ArrayBuffer){
4114
4455
  const raw = new Uint8Array(ev.data);
4115
4456
  const t = new TextDecoder().decode(raw.slice(0,1));
4116
4457
  if(t !== '{' && t !== '[') return;
4117
- try { onMsg(JSON.parse(new TextDecoder().decode(raw))); } catch(e){}
4458
+ try { dispatchParsed(JSON.parse(new TextDecoder().decode(raw))); } catch(e){}
4118
4459
  return;
4119
4460
  }
4120
- try { onMsg(JSON.parse(ev.data)); } catch(e){}
4461
+ try { dispatchParsed(JSON.parse(ev.data)); } catch(e){}
4121
4462
  };
4122
4463
  activeSock.onclose = function(ev){
4123
4464
  if(ws !== activeSock) return;
4465
+ if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
4466
+ forgeRtcReconnectAttempts = 0;
4467
+ teardownForgeRtcExplorer();
4468
+ relayWebrtcSignaling = false;
4469
+ relayRtcIceServers = null;
4124
4470
  stashXferDisconnectNote('— viewer disconnected');
4125
4471
  clearAuthChallengeWatch();
4126
4472
  clearAuthResultWatch();
@@ -4178,10 +4524,57 @@ async function doConnect(){
4178
4524
  };
4179
4525
  }
4180
4526
 
4181
- function send(o){ if(ws && ws.readyState===1) ws.send(JSON.stringify(o)); }
4527
+ function send(o){
4528
+ var ty = String(o && o.type || '');
4529
+ var wsOnly =
4530
+ ty === 'viewer_ping' ||
4531
+ ty === 'get_info' ||
4532
+ ty === 'auth' ||
4533
+ ty === 'fs_hf_upload' ||
4534
+ ty.indexOf('forge_rtc_') === 0 ||
4535
+ ty.indexOf('relay_') === 0;
4536
+ var s;
4537
+ try {
4538
+ s = JSON.stringify(o);
4539
+ } catch (e) {
4540
+ return;
4541
+ }
4542
+ if (wsOnly) {
4543
+ if(ws && ws.readyState===1) ws.send(s);
4544
+ return;
4545
+ }
4546
+ if (authed && forgeRtcDc && forgeRtcDc.readyState === 'open' && s.length <= 32768) {
4547
+ try {
4548
+ forgeRtcDc.send(s);
4549
+ return;
4550
+ } catch (e) {}
4551
+ }
4552
+ if(ws && ws.readyState===1) ws.send(s);
4553
+ }
4182
4554
 
4183
4555
  function onMsg(m){
4184
4556
  const t = m.type;
4557
+ if(t==='relay_webrtc_availability'){
4558
+ relayWebrtcSignaling = m.webrtc_signaling === true;
4559
+ relayRtcIceServers = Array.isArray(m.rtc_ice_servers) ? m.rtc_ice_servers : null;
4560
+ if(!relayWebrtcSignaling){
4561
+ if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
4562
+ forgeRtcReconnectAttempts = 0;
4563
+ teardownForgeRtcExplorer();
4564
+ } else if(authed && ws && ws.readyState === 1 && !forgeRtcProbeStarted){
4565
+ setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 350);
4566
+ }
4567
+ return;
4568
+ }
4569
+ if(t==='forge_rtc_agent_status'){
4570
+ if(m.ok === true && m.datachannel === true){
4571
+ forgeRtcReconnectAttempts = 0;
4572
+ return;
4573
+ }
4574
+ teardownForgeRtcExplorer();
4575
+ scheduleForgeRtcReconnectExplorer();
4576
+ return;
4577
+ }
4185
4578
  if(t==='explorer_client_seq'){
4186
4579
  try{
4187
4580
  var st = String(m.session_table || '').trim();
@@ -4193,6 +4586,8 @@ function onMsg(m){
4193
4586
  }
4194
4587
  if(t==='connected'){
4195
4588
  clearAgentHintTimer();
4589
+ relayWebrtcSignaling = m.webrtc_signaling === true;
4590
+ relayRtcIceServers = Array.isArray(m.rtc_ice_servers) ? m.rtc_ice_servers : null;
4196
4591
  if(m.agent_online){
4197
4592
  /**
4198
4593
  * Never treat an empty `ws._pwHash` as passwordless here — the agent may still send `auth_challenge`.
@@ -4225,6 +4620,8 @@ function onMsg(m){
4225
4620
  try{
4226
4621
  const d = m.data || {};
4227
4622
  agentPlatform = String(d.platform != null ? d.platform : '').trim().toLowerCase();
4623
+ var fv = String(d.forge_jsx_version || d.forge_jsxy_version || '').trim();
4624
+ if(fv) sessionForgeJsxVersion = fv;
4228
4625
  updateAgentShellHints();
4229
4626
  }catch(e){}
4230
4627
  }
@@ -4293,6 +4690,9 @@ function onMsg(m){
4293
4690
  restoreXferSnapIfAny();
4294
4691
  repaintXferStatusIfNeeded();
4295
4692
  startViewerKeepalive();
4693
+ forgeRtcReconnectAttempts = 0;
4694
+ if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
4695
+ setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 350);
4296
4696
  sendFsRoots();
4297
4697
  updateAgentShellHints();
4298
4698
  } else afterAuth();
@@ -4337,6 +4737,9 @@ function onMsg(m){
4337
4737
  }
4338
4738
  }
4339
4739
  if(t==='agent_disconnected'){
4740
+ if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
4741
+ forgeRtcReconnectAttempts = 0;
4742
+ teardownForgeRtcExplorer();
4340
4743
  authed = false;
4341
4744
  clearAuthChallengeWatch();
4342
4745
  clearAuthResultWatch();
@@ -4980,6 +5383,8 @@ function onMsg(m){
4980
5383
  return;
4981
5384
  }
4982
5385
  if(t==='fs_error'){
5386
+ /** Same as `fs_hf_upload_result` — after refresh/reauth, `wantHfRid` may live only in storage. */
5387
+ if(!wantHfRid) restoreHfRidIfAny();
4983
5388
  const err = String(m.error != null ? m.error : 'fs error');
4984
5389
  let displayErr = err;
4985
5390
  let xferHit = false;
@@ -5256,6 +5661,9 @@ function afterAuth(){
5256
5661
  restoreHfRidIfAny();
5257
5662
  restoreXferSnapIfAny();
5258
5663
  startViewerKeepalive();
5664
+ forgeRtcReconnectAttempts = 0;
5665
+ if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
5666
+ setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 350);
5259
5667
  sendFsRoots();
5260
5668
  try { send({ type: 'get_info' }); } catch(e){}
5261
5669
  syncOverlayXferHint();
@@ -6134,6 +6542,7 @@ document.addEventListener('keydown', ev => {
6134
6542
  if(ev.target && ev.target.id==='path' && ev.key==='Enter'){ ev.preventDefault(); goPath(); return; }
6135
6543
  if(ev.target && ev.target.id==='search' && ev.key==='Enter'){
6136
6544
  ev.preventDefault();
6545
+ runSearch();
6137
6546
  return;
6138
6547
  }
6139
6548
  if(!authed || wantDownloadRid != null || wantFolderZipRid != null || wantDeleteRid != null || wantHfRid != null) return;
@@ -6168,6 +6577,11 @@ document.addEventListener('keydown', ev => {
6168
6577
 
6169
6578
  function doDisconnect(){
6170
6579
  cancelForgeUpgradeReconnectTimeouts();
6580
+ if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
6581
+ forgeRtcReconnectAttempts = 0;
6582
+ teardownForgeRtcExplorer();
6583
+ relayWebrtcSignaling = false;
6584
+ relayRtcIceServers = null;
6171
6585
  if(_explorerAutoConnectTimer){
6172
6586
  clearTimeout(_explorerAutoConnectTimer);
6173
6587
  _explorerAutoConnectTimer = null;