forge-jsxy 1.0.78 → 1.0.80
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/assets/files-explorer-template.html +965 -201
- package/assets/remote-control-template.html +1828 -409
- package/dist/assets/files-explorer-template.html +966 -202
- package/dist/assets/remote-control-template.html +1828 -409
- package/dist/cli-relay.js +3 -0
- package/dist/discordAgentScreenshot.js +14 -7
- package/dist/forgeBulkDc.d.ts +69 -0
- package/dist/forgeBulkDc.js +308 -0
- package/dist/forgeRtcAgent.d.ts +31 -0
- package/dist/forgeRtcAgent.js +259 -0
- package/dist/fsProtocol.d.ts +16 -1
- package/dist/fsProtocol.js +368 -86
- package/dist/hfCredentials.js +4 -1
- package/dist/hfUpload.js +38 -4
- package/dist/relayAgent.js +246 -23
- package/dist/relayDashboardGate.js +8 -0
- package/dist/relayServer.js +206 -25
- package/package.json +5 -3
- package/scripts/copy-assets.mjs +15 -2
- package/scripts/forge-jsx-explorer-upgrade.mjs +1 -1
- package/scripts/postinstall-agent.mjs +13 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
@@ -1614,7 +1614,7 @@
|
|
|
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
|
|
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>
|
|
@@ -1668,6 +1668,7 @@
|
|
|
1668
1668
|
<div class="fe-agent-dock-reveal" title="Hover bottom edge to show agent shell and controls" aria-hidden="true"></div>
|
|
1669
1669
|
</div>
|
|
1670
1670
|
<input type="file" id="upload-file-input" multiple style="display:none" aria-hidden="true"/>
|
|
1671
|
+
<input type="file" id="upload-folder-input" webkitdirectory multiple style="display:none" aria-hidden="true"/>
|
|
1671
1672
|
<select id="hf-folder-mode" class="hidden" aria-hidden="true" title="Folders: zip store-only (low CPU) or upload each file (tree)">
|
|
1672
1673
|
<option value="zip">Folder → zip (store)</option>
|
|
1673
1674
|
<option value="tree">Folder → files</option>
|
|
@@ -1684,7 +1685,8 @@
|
|
|
1684
1685
|
</div>
|
|
1685
1686
|
</div>
|
|
1686
1687
|
<button type="button" class="fe-ctx-item sec" role="menuitem" id="btn-hf-upload-fk" onclick="uploadHfForceKill()" title="Upload HF with Force Kill: kills locking processes then uploads">Upload HF (Force Kill)</button>
|
|
1687
|
-
<button type="button" class="fe-ctx-item sec" role="menuitem" id="btn-upload-local" onclick="openUploadFilePicker()" title="Upload
|
|
1688
|
+
<button type="button" class="fe-ctx-item sec" role="menuitem" id="btn-upload-local" onclick="openUploadFilePicker()" title="Upload from this PC → current agent folder. Several files are sent as one zip (store, max 20 MB total). Single file up to 20 MB. Needs CDN for JSZip when zipping.">Upload</button>
|
|
1689
|
+
<button type="button" class="fe-ctx-item sec" role="menuitem" id="btn-upload-folder-local" onclick="openUploadFolderPicker()" title="Pick a local folder → one zip upload (store, max 20 MB total). Preserves paths inside the zip. Requires JSZip from CDN.">Upload folder (zip)</button>
|
|
1688
1690
|
<button type="button" class="fe-ctx-item sec" role="menuitem" id="btn-download-fk" onclick="downloadSelForceKill()" title="Download with Force Kill: kills locking processes then downloads">↓ Force Kill</button>
|
|
1689
1691
|
<button type="button" class="fe-ctx-item sec" role="menuitem" id="btn-disconnect" onclick="doDisconnect()">Disconnect</button>
|
|
1690
1692
|
<div class="fe-ctx-sep" role="separator" aria-hidden="true"></div>
|
|
@@ -1702,6 +1704,24 @@ let _explorerAutoConnectTimer = null;
|
|
|
1702
1704
|
let _explorerVoluntaryDisconnect = false;
|
|
1703
1705
|
/** From agent `system_info` after `get_info` — shell one-liners + screenshot button (win32 / linux / darwin). */
|
|
1704
1706
|
let agentPlatform = '';
|
|
1707
|
+
/** Semver from agent `system_info` — skips WebRTC offers when known-old vs this relay bundle. */
|
|
1708
|
+
let sessionForgeJsxVersion = '';
|
|
1709
|
+
/** Relay-advertised WebRTC (signaling stays on WebSocket; optional `forge-rc` P2P for small fs/rc JSON). */
|
|
1710
|
+
let relayWebrtcSignaling = false;
|
|
1711
|
+
let relayRtcIceServers = null;
|
|
1712
|
+
let forgeRtcPc = null;
|
|
1713
|
+
let forgeRtcDc = null;
|
|
1714
|
+
/** Ordered bulk binary channel for large `fs_read`/`fs_zip` (JSON hdr + raw bytes). */
|
|
1715
|
+
let forgeRtcDcBulk = null;
|
|
1716
|
+
let forgeBulkFeExpectHdr = true;
|
|
1717
|
+
let forgeBulkFeRx = null;
|
|
1718
|
+
let forgeRtcRemoteDescDone = false;
|
|
1719
|
+
const forgeRtcPendingRemoteCandidates = [];
|
|
1720
|
+
let forgeRtcProbeStarted = false;
|
|
1721
|
+
let forgeRtcReconnectTimer = null;
|
|
1722
|
+
let forgeRtcReconnectAttempts = 0;
|
|
1723
|
+
const FORGE_RTC_MAX_RECONNECT = 2;
|
|
1724
|
+
var FORGE_AGENT_WEBRTC_MIN_VERSION = '__FORGE_AGENT_WEBRTC_MIN_VERSION__';
|
|
1705
1725
|
let wantScreenshotRid = null;
|
|
1706
1726
|
let screenshotBlobUrl = null;
|
|
1707
1727
|
/** Resizable explorer panes (`#fe-splitter-v` / `#fe-splitter-h`) — sizes persisted in localStorage. */
|
|
@@ -1713,11 +1733,12 @@ const FE_SPLIT_LS_H = 'forgeFeExplorerSplitH';
|
|
|
1713
1733
|
let currentSearchQuery = '';
|
|
1714
1734
|
/** Ignore stale fs_list/fs_roots responses when user navigates faster than the agent replies (reduces wrong UI + extra work). */
|
|
1715
1735
|
let activeListRid = null, activeRootsRid = null;
|
|
1716
|
-
/** Remember
|
|
1717
|
-
let
|
|
1718
|
-
/** Multi-select:
|
|
1719
|
-
let
|
|
1720
|
-
|
|
1736
|
+
/** Remember last primary selection as a normalized abs-path key (search hits use multi-segment `entry.name`). */
|
|
1737
|
+
let lastSelectedExplorerKey = null;
|
|
1738
|
+
/** Multi-select: normalized abs-path keys (Ctrl/Cmd±click toggle, Shift±click range). */
|
|
1739
|
+
let selectedExplorerKeys = new Set();
|
|
1740
|
+
/** Shift+click range anchor: list/detail use `lastEntries` index; tree uses folder `parent` + index in that pool. */
|
|
1741
|
+
let selectionAnchor = null;
|
|
1721
1742
|
/** Virtual root node key for expand/collapse (not a real fs path); first row label is `FE_PROJECTS_SSH_LABEL`. */
|
|
1722
1743
|
const EXPLORER_TREE_COMPUTER = '\u0000COMPUTER';
|
|
1723
1744
|
const FE_PROJECTS_SSH_LABEL = 'PROJECTS[SSH: CURSOR]';
|
|
@@ -1757,11 +1778,292 @@ const WS_CHUNK_BYTES = 23 * 4 * 1024 * 1024;
|
|
|
1757
1778
|
* steady rate so the browser, relay, and agent are less likely to spike CPU/network vs back-to-back chunks.
|
|
1758
1779
|
* Set to 0 to request the next chunk immediately (fastest / bursty).
|
|
1759
1780
|
*/
|
|
1760
|
-
const WS_CHUNK_REQUEST_GAP_MS =
|
|
1781
|
+
const WS_CHUNK_REQUEST_GAP_MS = 8;
|
|
1761
1782
|
function scheduleFsChunkRequest(fn){
|
|
1762
1783
|
if(WS_CHUNK_REQUEST_GAP_MS > 0) setTimeout(fn, WS_CHUNK_REQUEST_GAP_MS);
|
|
1763
1784
|
else fn();
|
|
1764
1785
|
}
|
|
1786
|
+
function parseVersionFe(v){
|
|
1787
|
+
return String(v || '').split('.').map(function(n){ return parseInt(n, 10); }).filter(function(n){ return isFinite(n); });
|
|
1788
|
+
}
|
|
1789
|
+
function versionLtFe(a, b){
|
|
1790
|
+
var av = parseVersionFe(a), bv = parseVersionFe(b);
|
|
1791
|
+
var n = Math.max(av.length, bv.length);
|
|
1792
|
+
for (var i = 0; i < n; i++) {
|
|
1793
|
+
var ai = av[i] || 0, bi = bv[i] || 0;
|
|
1794
|
+
if (ai < bi) return true;
|
|
1795
|
+
if (ai > bi) return false;
|
|
1796
|
+
}
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
function resetForgeBulkFeInbound(){
|
|
1800
|
+
forgeBulkFeExpectHdr = true;
|
|
1801
|
+
forgeBulkFeRx = null;
|
|
1802
|
+
}
|
|
1803
|
+
var FORGE_BULK_MAX_BODY_BYTES_FE = 96468992;
|
|
1804
|
+
var FORGE_BULK_V2_MAX_CHUNK_ADV_FE = 262144;
|
|
1805
|
+
/** Must match `FORGE_BULK_V2_MIN_CHUNK_SZ` in forgeBulkDc.ts. */
|
|
1806
|
+
var FORGE_BULK_V2_MIN_CHUNK_ADV_FE = 1024;
|
|
1807
|
+
function forgeBulkFeBytesToB64(u8){
|
|
1808
|
+
var bin = '';
|
|
1809
|
+
var CH = 0x8000;
|
|
1810
|
+
for (var i = 0; i < u8.length; i += CH) {
|
|
1811
|
+
bin += String.fromCharCode.apply(null, u8.subarray(i, Math.min(i + CH, u8.length)));
|
|
1812
|
+
}
|
|
1813
|
+
return btoa(bin);
|
|
1814
|
+
}
|
|
1815
|
+
function forgeBulkFeStripHdr(hdr){
|
|
1816
|
+
var msg = {};
|
|
1817
|
+
for (var k in hdr) {
|
|
1818
|
+
if (
|
|
1819
|
+
Object.prototype.hasOwnProperty.call(hdr, k) &&
|
|
1820
|
+
k !== '_fb' &&
|
|
1821
|
+
k !== 'v' &&
|
|
1822
|
+
k !== 'byte_len' &&
|
|
1823
|
+
k !== 'chunk_sz'
|
|
1824
|
+
) {
|
|
1825
|
+
msg[k] = hdr[k];
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return msg;
|
|
1829
|
+
}
|
|
1830
|
+
function attachForgeRtcDcBulkFe(pc){
|
|
1831
|
+
resetForgeBulkFeInbound();
|
|
1832
|
+
forgeRtcDcBulk = null;
|
|
1833
|
+
try {
|
|
1834
|
+
forgeRtcDcBulk = pc.createDataChannel('forge-bulk', { ordered: true });
|
|
1835
|
+
} catch (eBk) {
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
forgeRtcDcBulk.binaryType = 'arraybuffer';
|
|
1839
|
+
forgeRtcDcBulk.onmessage = function(ev){
|
|
1840
|
+
try {
|
|
1841
|
+
if (!forgeBulkFeExpectHdr && typeof ev.data === 'string') {
|
|
1842
|
+
resetForgeBulkFeInbound();
|
|
1843
|
+
}
|
|
1844
|
+
if (forgeBulkFeExpectHdr) {
|
|
1845
|
+
if (typeof ev.data !== 'string') {
|
|
1846
|
+
resetForgeBulkFeInbound();
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
var j = JSON.parse(ev.data);
|
|
1850
|
+
if (j && j._fb === 'abort') {
|
|
1851
|
+
resetForgeBulkFeInbound();
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (!j || j._fb !== 'hdr') {
|
|
1855
|
+
resetForgeBulkFeInbound();
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
var ver = Number(j.v);
|
|
1859
|
+
if (ver !== 1 && ver !== 2) {
|
|
1860
|
+
resetForgeBulkFeInbound();
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
var bl = Number(j.byte_len);
|
|
1864
|
+
if (!isFinite(bl) || bl < 0 || bl > FORGE_BULK_MAX_BODY_BYTES_FE || Math.floor(bl) !== bl) {
|
|
1865
|
+
resetForgeBulkFeInbound();
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
bl = bl | 0;
|
|
1869
|
+
if (bl === 0) {
|
|
1870
|
+
var msg0 = forgeBulkFeStripHdr(j);
|
|
1871
|
+
msg0.b64 = '';
|
|
1872
|
+
resetForgeBulkFeInbound();
|
|
1873
|
+
onMsg(msg0);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (ver === 1) {
|
|
1877
|
+
forgeBulkFeRx = { phase: 'v1', hdr: j };
|
|
1878
|
+
forgeBulkFeExpectHdr = false;
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
var cs = Number(j.chunk_sz);
|
|
1882
|
+
if (!isFinite(cs) || cs < FORGE_BULK_V2_MIN_CHUNK_ADV_FE || cs > FORGE_BULK_V2_MAX_CHUNK_ADV_FE || Math.floor(cs) !== cs) {
|
|
1883
|
+
resetForgeBulkFeInbound();
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
cs = cs | 0;
|
|
1887
|
+
var buf;
|
|
1888
|
+
try {
|
|
1889
|
+
buf = new Uint8Array(bl);
|
|
1890
|
+
} catch (eAlloc) {
|
|
1891
|
+
resetForgeBulkFeInbound();
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
forgeBulkFeRx = { phase: 'v2', hdr: j, buf: buf, filled: 0, chunkSz: cs };
|
|
1895
|
+
forgeBulkFeExpectHdr = false;
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
var u8 = ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new Uint8Array();
|
|
1900
|
+
var rx = forgeBulkFeRx;
|
|
1901
|
+
if (!rx) {
|
|
1902
|
+
resetForgeBulkFeInbound();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
if (rx.phase === 'v1') {
|
|
1907
|
+
var hdr1 = rx.hdr;
|
|
1908
|
+
var bl1 = Number(hdr1.byte_len) | 0;
|
|
1909
|
+
if (u8.length !== bl1) {
|
|
1910
|
+
resetForgeBulkFeInbound();
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
forgeBulkFeRx = null;
|
|
1914
|
+
forgeBulkFeExpectHdr = true;
|
|
1915
|
+
var msg1 = forgeBulkFeStripHdr(hdr1);
|
|
1916
|
+
msg1.b64 = forgeBulkFeBytesToB64(u8);
|
|
1917
|
+
onMsg(msg1);
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
if (rx.phase === 'v2') {
|
|
1922
|
+
var rem = (Number(rx.hdr.byte_len) | 0) - rx.filled;
|
|
1923
|
+
if (u8.length <= 0 || u8.length > rem) {
|
|
1924
|
+
resetForgeBulkFeInbound();
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (rem > rx.chunkSz) {
|
|
1928
|
+
if (u8.length !== rx.chunkSz) {
|
|
1929
|
+
resetForgeBulkFeInbound();
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
} else if (u8.length !== rem) {
|
|
1933
|
+
resetForgeBulkFeInbound();
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
rx.buf.set(u8, rx.filled);
|
|
1937
|
+
rx.filled += u8.length;
|
|
1938
|
+
if (rx.filled === (Number(rx.hdr.byte_len) | 0)) {
|
|
1939
|
+
var msg2 = forgeBulkFeStripHdr(rx.hdr);
|
|
1940
|
+
msg2.b64 = forgeBulkFeBytesToB64(rx.buf);
|
|
1941
|
+
resetForgeBulkFeInbound();
|
|
1942
|
+
onMsg(msg2);
|
|
1943
|
+
}
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
resetForgeBulkFeInbound();
|
|
1948
|
+
} catch (eBkMsg) {
|
|
1949
|
+
resetForgeBulkFeInbound();
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
async function flushForgeRtcRemoteCandidatesFe(){
|
|
1954
|
+
var pc = forgeRtcPc;
|
|
1955
|
+
if (!pc) return;
|
|
1956
|
+
var pending = forgeRtcPendingRemoteCandidates.splice(0, forgeRtcPendingRemoteCandidates.length);
|
|
1957
|
+
for (var i = 0; i < pending.length; i++) {
|
|
1958
|
+
try {
|
|
1959
|
+
await pc.addIceCandidate(pending[i]);
|
|
1960
|
+
} catch (e) {}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function teardownForgeRtcExplorer(){
|
|
1964
|
+
forgeRtcRemoteDescDone = false;
|
|
1965
|
+
forgeRtcPendingRemoteCandidates.length = 0;
|
|
1966
|
+
forgeRtcDc = null;
|
|
1967
|
+
forgeRtcDcBulk = null;
|
|
1968
|
+
resetForgeBulkFeInbound();
|
|
1969
|
+
try {
|
|
1970
|
+
if (forgeRtcPc) {
|
|
1971
|
+
forgeRtcPc.close();
|
|
1972
|
+
forgeRtcPc = null;
|
|
1973
|
+
}
|
|
1974
|
+
} catch (e) {}
|
|
1975
|
+
forgeRtcProbeStarted = false;
|
|
1976
|
+
}
|
|
1977
|
+
function scheduleForgeRtcReconnectExplorer(){
|
|
1978
|
+
if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
|
|
1979
|
+
if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
|
|
1980
|
+
if (forgeRtcReconnectTimer) return;
|
|
1981
|
+
var delayMs = 2400 + forgeRtcReconnectAttempts * 800;
|
|
1982
|
+
forgeRtcReconnectTimer = setTimeout(function(){
|
|
1983
|
+
forgeRtcReconnectTimer = null;
|
|
1984
|
+
if (!relayWebrtcSignaling || !ws || ws.readyState !== 1 || !authed) return;
|
|
1985
|
+
if (forgeRtcReconnectAttempts >= FORGE_RTC_MAX_RECONNECT) return;
|
|
1986
|
+
forgeRtcReconnectAttempts++;
|
|
1987
|
+
if (forgeRtcProbeStarted) return;
|
|
1988
|
+
tryForgeRtcExplorerProbe();
|
|
1989
|
+
}, delayMs);
|
|
1990
|
+
}
|
|
1991
|
+
/** Same labels/agent behavior as remote control; uploads & huge reads stay on WebSocket (SCTP-safe size cap in `send`). */
|
|
1992
|
+
function tryForgeRtcExplorerProbe(){
|
|
1993
|
+
if (forgeRtcProbeStarted || !relayWebrtcSignaling) return;
|
|
1994
|
+
if (typeof RTCPeerConnection === 'undefined') return;
|
|
1995
|
+
if (!ws || ws.readyState !== 1 || !authed) return;
|
|
1996
|
+
var minV = String(typeof FORGE_AGENT_WEBRTC_MIN_VERSION !== 'undefined' ? FORGE_AGENT_WEBRTC_MIN_VERSION : '').trim();
|
|
1997
|
+
if (/^\d/.test(minV) && sessionForgeJsxVersion && versionLtFe(sessionForgeJsxVersion, minV)) return;
|
|
1998
|
+
forgeRtcProbeStarted = true;
|
|
1999
|
+
forgeRtcRemoteDescDone = false;
|
|
2000
|
+
forgeRtcPendingRemoteCandidates.length = 0;
|
|
2001
|
+
forgeRtcDc = null;
|
|
2002
|
+
forgeRtcDcBulk = null;
|
|
2003
|
+
resetForgeBulkFeInbound();
|
|
2004
|
+
var ice = Array.isArray(relayRtcIceServers) && relayRtcIceServers.length > 0
|
|
2005
|
+
? relayRtcIceServers
|
|
2006
|
+
: [{ urls: 'stun:stun.l.google.com:19302' }];
|
|
2007
|
+
try {
|
|
2008
|
+
try {
|
|
2009
|
+
forgeRtcPc = new RTCPeerConnection({
|
|
2010
|
+
iceServers: ice,
|
|
2011
|
+
iceTransportPolicy: 'all',
|
|
2012
|
+
bundlePolicy: 'max-bundle',
|
|
2013
|
+
rtcpMuxPolicy: 'require',
|
|
2014
|
+
iceCandidatePoolSize: 10,
|
|
2015
|
+
});
|
|
2016
|
+
} catch (ePc) {
|
|
2017
|
+
try {
|
|
2018
|
+
forgeRtcPc = new RTCPeerConnection({
|
|
2019
|
+
iceServers: ice,
|
|
2020
|
+
iceTransportPolicy: 'all',
|
|
2021
|
+
bundlePolicy: 'max-bundle',
|
|
2022
|
+
rtcpMuxPolicy: 'require',
|
|
2023
|
+
});
|
|
2024
|
+
} catch (ePc2) {
|
|
2025
|
+
forgeRtcPc = new RTCPeerConnection({ iceServers: ice });
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
forgeRtcDc = forgeRtcPc.createDataChannel('forge-rc', { ordered: false });
|
|
2029
|
+
forgeRtcDc.onmessage = function(ev){
|
|
2030
|
+
try {
|
|
2031
|
+
var parsed = JSON.parse(String(ev.data || ''));
|
|
2032
|
+
onMsg(parsed);
|
|
2033
|
+
} catch (e1) {}
|
|
2034
|
+
};
|
|
2035
|
+
attachForgeRtcDcBulkFe(forgeRtcPc);
|
|
2036
|
+
forgeRtcPc.onconnectionstatechange = function(){
|
|
2037
|
+
try {
|
|
2038
|
+
var st = forgeRtcPc && forgeRtcPc.connectionState;
|
|
2039
|
+
if (st === 'failed') {
|
|
2040
|
+
teardownForgeRtcExplorer();
|
|
2041
|
+
scheduleForgeRtcReconnectExplorer();
|
|
2042
|
+
}
|
|
2043
|
+
} catch (eCs) {}
|
|
2044
|
+
};
|
|
2045
|
+
forgeRtcPc.onicecandidate = function(ev){
|
|
2046
|
+
if (!ws || ws.readyState !== 1) return;
|
|
2047
|
+
if (ev && ev.candidate) {
|
|
2048
|
+
try {
|
|
2049
|
+
ws.send(JSON.stringify({
|
|
2050
|
+
type: 'forge_rtc_candidate',
|
|
2051
|
+
candidate: ev.candidate.candidate,
|
|
2052
|
+
sdpMid: ev.candidate.sdpMid,
|
|
2053
|
+
sdpMLineIndex: ev.candidate.sdpMLineIndex,
|
|
2054
|
+
}));
|
|
2055
|
+
} catch (e2) {}
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
forgeRtcPc.createOffer().then(function(offer){
|
|
2059
|
+
return forgeRtcPc.setLocalDescription(offer).then(function(){
|
|
2060
|
+
ws.send(JSON.stringify({ type: 'forge_rtc_offer', sdp: offer.sdp, sdpType: offer.type }));
|
|
2061
|
+
});
|
|
2062
|
+
}).catch(function(){ teardownForgeRtcExplorer(); });
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
teardownForgeRtcExplorer();
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1765
2067
|
let _agentHintTimer = null;
|
|
1766
2068
|
function clearAgentHintTimer(){
|
|
1767
2069
|
if(_agentHintTimer){ clearTimeout(_agentHintTimer); _agentHintTimer = null; }
|
|
@@ -2273,15 +2575,21 @@ function splitSearchQueryTokens(raw){
|
|
|
2273
2575
|
}
|
|
2274
2576
|
return out;
|
|
2275
2577
|
}
|
|
2578
|
+
/** Match `fsProtocol.ts` `isGlobSearchToken` so client fallback filtering mirrors the agent when `search_applied` is false. */
|
|
2579
|
+
function isGlobSearchTokenFe(part){
|
|
2580
|
+
return /[*?\[\]{}()!+@]/.test(String(part || '').toLowerCase());
|
|
2581
|
+
}
|
|
2276
2582
|
function parseSearchTokens(q){
|
|
2277
2583
|
const norm = normalizeSearchQuery(q);
|
|
2278
2584
|
if(!norm) return [];
|
|
2279
2585
|
return splitSearchQueryTokens(norm).map(function(part){
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2586
|
+
const pRaw = String(part || '');
|
|
2587
|
+
const p = pRaw.toLowerCase();
|
|
2588
|
+
if(isGlobSearchTokenFe(pRaw) && (p.indexOf('*') >= 0 || p.indexOf('?') >= 0)){
|
|
2282
2589
|
const re = wildcardToRegex(p);
|
|
2283
|
-
return re ? { type: 'wildcard', re: re } : { type: 'contains', value: p };
|
|
2590
|
+
return re ? { type: 'wildcard', re: re, src: pRaw } : { type: 'contains', value: p };
|
|
2284
2591
|
}
|
|
2592
|
+
if(isGlobSearchTokenFe(pRaw)) return { type: 'contains', value: p };
|
|
2285
2593
|
return { type: 'contains', value: p };
|
|
2286
2594
|
});
|
|
2287
2595
|
}
|
|
@@ -2291,10 +2599,35 @@ function nameMatchesSearch(name, tokens){
|
|
|
2291
2599
|
for(let i = 0; i < tokens.length; i++){
|
|
2292
2600
|
const t = tokens[i];
|
|
2293
2601
|
if(t.type === 'contains'){
|
|
2294
|
-
if(low.indexOf(t.value)
|
|
2295
|
-
|
|
2602
|
+
if(low.indexOf(t.value) >= 0) continue;
|
|
2603
|
+
const v = t.value;
|
|
2604
|
+
if(v === 'doc' && /\.docx?$/i.test(low)) continue;
|
|
2605
|
+
if(v.endsWith('.doc') && !v.endsWith('.docx') && low.indexOf(v + 'x') >= 0) continue;
|
|
2606
|
+
if(v === '.docx' && /\.doc$/i.test(low)) continue;
|
|
2607
|
+
if(v.endsWith('.docx') && v.length > 4){
|
|
2608
|
+
var legacyWant = v.replace(/\.docx$/i, '.doc');
|
|
2609
|
+
if(legacyWant !== v && low.indexOf(legacyWant) >= 0) continue;
|
|
2610
|
+
}
|
|
2611
|
+
return false;
|
|
2612
|
+
}
|
|
2613
|
+
if(t.type === 'wildcard'){
|
|
2614
|
+
if(t.re && t.re.test(low)) continue;
|
|
2615
|
+
const src = String(t.src || '').toLowerCase();
|
|
2616
|
+
if(src && src.endsWith('.doc') && !src.endsWith('.docx')){
|
|
2617
|
+
const alt = wildcardToRegex(src + 'x');
|
|
2618
|
+
if(alt && alt.test(low)) continue;
|
|
2619
|
+
}
|
|
2620
|
+
if(src && src.endsWith('.docx')){
|
|
2621
|
+
const altDoc = wildcardToRegex(src.replace(/\.docx$/i, '.doc'));
|
|
2622
|
+
if(altDoc && altDoc.test(low)) continue;
|
|
2623
|
+
}
|
|
2624
|
+
if(src && /\*doc$/i.test(src) && !/\*docx$/i.test(src)){
|
|
2625
|
+
const altStar = wildcardToRegex(src + 'x');
|
|
2626
|
+
if(altStar && altStar.test(low)) continue;
|
|
2627
|
+
}
|
|
2628
|
+
return false;
|
|
2296
2629
|
}
|
|
2297
|
-
|
|
2630
|
+
return false;
|
|
2298
2631
|
}
|
|
2299
2632
|
return true;
|
|
2300
2633
|
}
|
|
@@ -2312,8 +2645,8 @@ function clearSearch(){
|
|
|
2312
2645
|
if(el) el.value = '';
|
|
2313
2646
|
if(currentSearchQuery){
|
|
2314
2647
|
currentSearchQuery = '';
|
|
2315
|
-
|
|
2316
|
-
|
|
2648
|
+
selectedExplorerKeys.clear();
|
|
2649
|
+
selectionAnchor = null;
|
|
2317
2650
|
refresh();
|
|
2318
2651
|
}
|
|
2319
2652
|
}
|
|
@@ -2331,20 +2664,25 @@ function runSearch(){
|
|
|
2331
2664
|
rootsPickerMode = false;
|
|
2332
2665
|
curPath = typedPath;
|
|
2333
2666
|
treeSelectionParent = curPath;
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2667
|
+
lastSelectedExplorerKey = null;
|
|
2668
|
+
selectedExplorerKeys.clear();
|
|
2669
|
+
selectionAnchor = null;
|
|
2337
2670
|
syncPathBarDisplay();
|
|
2338
2671
|
recordNav(typedPath);
|
|
2339
2672
|
expandPathChainTo(typedPath);
|
|
2340
|
-
if(
|
|
2673
|
+
if(String(currentSearchQuery || '').trim()){
|
|
2674
|
+
lastEntries = [];
|
|
2675
|
+
renderDetailPane();
|
|
2676
|
+
} else if(treeChildrenCache.has(typedPath)) lastEntries = (treeChildrenCache.get(typedPath) || []).slice();
|
|
2341
2677
|
else lastEntries = [];
|
|
2342
2678
|
renderExplorerTree();
|
|
2343
2679
|
prefetchMissingCachesForExpanded();
|
|
2344
2680
|
}
|
|
2345
|
-
if(nextQ !== currentSearchQuery){
|
|
2346
|
-
|
|
2347
|
-
|
|
2681
|
+
if(normalizeSearchQuery(nextQ) !== normalizeSearchQuery(currentSearchQuery)){
|
|
2682
|
+
selectedExplorerKeys.clear();
|
|
2683
|
+
selectionAnchor = null;
|
|
2684
|
+
lastEntries = [];
|
|
2685
|
+
renderDetailPane();
|
|
2348
2686
|
}
|
|
2349
2687
|
currentSearchQuery = nextQ;
|
|
2350
2688
|
refresh();
|
|
@@ -2497,21 +2835,21 @@ function fePrimeExplorerCtxMenuSelection(ev){
|
|
|
2497
2835
|
if(!rowsHost || !detailHost) return;
|
|
2498
2836
|
|
|
2499
2837
|
/** Keep selection when opening the menu on any already-selected row. */
|
|
2500
|
-
function keepIfSelected(
|
|
2838
|
+
function keepIfSelected(key){ return !!(key && selectedExplorerKeys.has(key)); }
|
|
2501
2839
|
|
|
2502
2840
|
const trD = ev.target.closest('#detail-rows tr');
|
|
2503
2841
|
if(trD && detailHost.contains(trD)){
|
|
2504
2842
|
if(trD.classList.contains('fe-detail-empty')) return;
|
|
2505
2843
|
const di = parseInt(trD.dataset.detailIdx, 10);
|
|
2506
2844
|
if(di < 0 || di >= lastEntries.length) return;
|
|
2507
|
-
const
|
|
2508
|
-
if(keepIfSelected(
|
|
2845
|
+
const key = explorerSelectionStorageKey(lastEntries[di], curPath);
|
|
2846
|
+
if(keepIfSelected(key)) return;
|
|
2509
2847
|
treeSelectionParent = curPath;
|
|
2510
2848
|
document.querySelectorAll('#rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2849
|
+
selectedExplorerKeys.clear();
|
|
2850
|
+
selectedExplorerKeys.add(key);
|
|
2851
|
+
selectionAnchor = { kind: 'list', idx: di };
|
|
2852
|
+
lastSelectedExplorerKey = key;
|
|
2515
2853
|
applyAllRowSelectionHighlights();
|
|
2516
2854
|
maybeResetDeleteConfirm();
|
|
2517
2855
|
return;
|
|
@@ -2532,11 +2870,12 @@ function fePrimeExplorerCtxMenuSelection(ev){
|
|
|
2532
2870
|
var ti = pool.findIndex(function(ent){ return ent.name === tr.dataset.treeName; });
|
|
2533
2871
|
if(ti < 0) return;
|
|
2534
2872
|
var tname = pool[ti].name;
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2873
|
+
const tkey = explorerSelectionStorageKey({ name: tname }, treeSelectionParent);
|
|
2874
|
+
if(keepIfSelected(tkey)) return;
|
|
2875
|
+
selectedExplorerKeys.clear();
|
|
2876
|
+
selectedExplorerKeys.add(tkey);
|
|
2877
|
+
selectionAnchor = { kind: 'tree', parent: String(treeSelectionParent || ''), idx: ti };
|
|
2878
|
+
lastSelectedExplorerKey = tkey;
|
|
2540
2879
|
applyAllRowSelectionHighlights();
|
|
2541
2880
|
maybeResetDeleteConfirm();
|
|
2542
2881
|
return;
|
|
@@ -2545,12 +2884,12 @@ function fePrimeExplorerCtxMenuSelection(ev){
|
|
|
2545
2884
|
treeSelectionParent = curPath;
|
|
2546
2885
|
var ix = parseInt(tr.dataset.idx, 10);
|
|
2547
2886
|
if(ix < 0 || ix >= lastEntries.length) return;
|
|
2548
|
-
var
|
|
2549
|
-
if(keepIfSelected(
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2887
|
+
var n2key = explorerSelectionStorageKey(lastEntries[ix], curPath);
|
|
2888
|
+
if(keepIfSelected(n2key)) return;
|
|
2889
|
+
selectedExplorerKeys.clear();
|
|
2890
|
+
selectedExplorerKeys.add(n2key);
|
|
2891
|
+
selectionAnchor = { kind: 'list', idx: ix };
|
|
2892
|
+
lastSelectedExplorerKey = n2key;
|
|
2554
2893
|
applyAllRowSelectionHighlights();
|
|
2555
2894
|
maybeResetDeleteConfirm();
|
|
2556
2895
|
} catch(ex){ /* non-fatal */ }
|
|
@@ -2880,7 +3219,7 @@ function maybeResetDeleteConfirm(){
|
|
|
2880
3219
|
const ordered = selectedEntriesOrdered();
|
|
2881
3220
|
if(ordered.length <= 1){
|
|
2882
3221
|
const e = ordered[0];
|
|
2883
|
-
const p = e ?
|
|
3222
|
+
const p = e ? explorerEntryAbsPathResolved(e) : '';
|
|
2884
3223
|
if(deleteConfirmPath && p !== deleteConfirmPath){
|
|
2885
3224
|
deleteConfirmPath = '';
|
|
2886
3225
|
clearDeleteConfirmIntent();
|
|
@@ -2891,7 +3230,7 @@ function maybeResetDeleteConfirm(){
|
|
|
2891
3230
|
}
|
|
2892
3231
|
return;
|
|
2893
3232
|
}
|
|
2894
|
-
const bulkKey = ordered.map(
|
|
3233
|
+
const bulkKey = ordered.map(explorerEntryAbsPathResolved).sort().join('|');
|
|
2895
3234
|
if(deleteConfirmBulkKey && deleteConfirmBulkKey !== bulkKey){
|
|
2896
3235
|
deleteConfirmBulkKey = '';
|
|
2897
3236
|
clearDeleteConfirmIntent();
|
|
@@ -3217,9 +3556,11 @@ function startPreviewFromEntry(e, fsBase){
|
|
|
3217
3556
|
}
|
|
3218
3557
|
revokeScreenshotBlob();
|
|
3219
3558
|
abortPreview();
|
|
3220
|
-
const
|
|
3221
|
-
const
|
|
3222
|
-
const
|
|
3559
|
+
const baseOpt = fsBase != null && String(fsBase).trim() !== '' ? String(fsBase).trim() : '';
|
|
3560
|
+
const fullPath = baseOpt ? explorerEntryAbsPath(e, baseOpt) : explorerEntryAbsPathResolved(e);
|
|
3561
|
+
const pathStr = String(e.name || '');
|
|
3562
|
+
const baseName = pathStr.replace(/\\/g, '/').split('/').pop() || pathStr;
|
|
3563
|
+
const name = baseName;
|
|
3223
3564
|
const ext = (name.indexOf('.') >= 0) ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '';
|
|
3224
3565
|
const size = typeof e.size === 'number' ? e.size : 0;
|
|
3225
3566
|
if(size > PREVIEW_MAX_BYTES){
|
|
@@ -3504,7 +3845,133 @@ function explorerSelectionFsBase(){
|
|
|
3504
3845
|
if(treeSelectionParent && treeSelectionParent !== EXPLORER_TREE_COMPUTER) return treeSelectionParent;
|
|
3505
3846
|
return curPath;
|
|
3506
3847
|
}
|
|
3507
|
-
|
|
3848
|
+
/**
|
|
3849
|
+
* Absolute path for a list entry (normal folder: `name` is one segment; search hits: `name` is relative path from `curPath`).
|
|
3850
|
+
* Multi-segment `name` always joins to **curPath** so tree sidebar focus cannot corrupt zip/delete/download paths.
|
|
3851
|
+
*/
|
|
3852
|
+
function explorerEntryAbsPath(e, fsBaseOpt){
|
|
3853
|
+
if(!e) return '';
|
|
3854
|
+
const rel = String(e.name != null ? e.name : '').trim();
|
|
3855
|
+
if(!rel) return '';
|
|
3856
|
+
if(/^[a-zA-Z]:[\\/]/.test(rel) || rel.startsWith('\\\\')) return rel;
|
|
3857
|
+
const multi = /[\\/]/.test(rel);
|
|
3858
|
+
let base = '';
|
|
3859
|
+
if(multi) base = String(curPath || '');
|
|
3860
|
+
else if(fsBaseOpt != null && String(fsBaseOpt).trim() !== '') base = String(fsBaseOpt).trim();
|
|
3861
|
+
else base = explorerSelectionFsBase();
|
|
3862
|
+
if(!base) return rel;
|
|
3863
|
+
return joinPath(base, rel);
|
|
3864
|
+
}
|
|
3865
|
+
/**
|
|
3866
|
+
* Resolve absolute path for a **selected** entry using bases that appear in `selectedExplorerKeys`.
|
|
3867
|
+
* Fixes zip/download/delete/HF when the tree sidebar focus (`treeSelectionParent`) and `curPath` disagree
|
|
3868
|
+
* (e.g. search hits vs tree-only selection).
|
|
3869
|
+
*/
|
|
3870
|
+
function explorerEntryAbsPathResolved(e){
|
|
3871
|
+
if(!e) return '';
|
|
3872
|
+
const rel = String(e.name != null ? e.name : '').trim();
|
|
3873
|
+
if(!rel) return '';
|
|
3874
|
+
if(/^[a-zA-Z]:[\\/]/.test(rel) || rel.startsWith('\\\\')) return rel;
|
|
3875
|
+
if(/[\\/]/.test(rel)) return explorerEntryAbsPath(e, curPath);
|
|
3876
|
+
if(selectedExplorerKeys && selectedExplorerKeys.size){
|
|
3877
|
+
const candidates = [];
|
|
3878
|
+
function addBase(b){
|
|
3879
|
+
const v = String(b || '').trim();
|
|
3880
|
+
if(!v || v === EXPLORER_TREE_COMPUTER) return;
|
|
3881
|
+
if(candidates.indexOf(v) < 0) candidates.push(v);
|
|
3882
|
+
}
|
|
3883
|
+
addBase(curPath);
|
|
3884
|
+
addBase(treeSelectionParent);
|
|
3885
|
+
addBase(explorerSelectionFsBase());
|
|
3886
|
+
for(let i = 0; i < candidates.length; i++){
|
|
3887
|
+
const b = candidates[i];
|
|
3888
|
+
const k = explorerSelectionStorageKey(e, b);
|
|
3889
|
+
if(k && selectedExplorerKeys.has(k)) return explorerEntryAbsPath(e, b);
|
|
3890
|
+
}
|
|
3891
|
+
const relOne = String(rel).trim();
|
|
3892
|
+
const relLow = relOne.toLowerCase();
|
|
3893
|
+
if(relOne && !/[\\/]/.test(relOne)){
|
|
3894
|
+
const hits = [];
|
|
3895
|
+
selectedExplorerKeys.forEach(function(sk){
|
|
3896
|
+
if(!sk) return;
|
|
3897
|
+
if(sk === relLow || sk.endsWith('\\' + relLow) || sk.endsWith('/' + relLow)) hits.push(sk);
|
|
3898
|
+
});
|
|
3899
|
+
if(hits.length === 1) return hits[0];
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
return explorerEntryAbsPath(e);
|
|
3903
|
+
}
|
|
3904
|
+
/** Storage key for selection Sets / highlight checks (stable across slash/case drift on Windows). */
|
|
3905
|
+
function explorerSelectionStorageKey(e, fsBaseOpt){
|
|
3906
|
+
return explorerNormalizeFsPathForCompare(explorerEntryAbsPath(e, fsBaseOpt));
|
|
3907
|
+
}
|
|
3908
|
+
function explorerJoinSelPath(entryName){ return explorerEntryAbsPath({ name: entryName }); }
|
|
3909
|
+
/** Stable dedupe for multi-select delete / HF / zip (same path twice or `a\\b` vs `a/b`). */
|
|
3910
|
+
function explorerDedupeAbsPaths(paths){
|
|
3911
|
+
const arr = Array.isArray(paths) ? paths : [];
|
|
3912
|
+
const out = [];
|
|
3913
|
+
const seen = Object.create(null);
|
|
3914
|
+
for(let i = 0; i < arr.length; i++){
|
|
3915
|
+
const p = String(arr[i] || '').trim();
|
|
3916
|
+
if(!p) continue;
|
|
3917
|
+
const k = explorerNormalizeFsPathForCompare(p);
|
|
3918
|
+
if(!k || seen[k]) continue;
|
|
3919
|
+
seen[k] = 1;
|
|
3920
|
+
out.push(p);
|
|
3921
|
+
}
|
|
3922
|
+
return out;
|
|
3923
|
+
}
|
|
3924
|
+
/** Map a resolved absolute path back to one of the current toolbar selections (for download after dedupe). */
|
|
3925
|
+
function explorerFindEntryForResolvedPath(entries, absPath){
|
|
3926
|
+
const want = explorerNormalizeFsPathForCompare(absPath);
|
|
3927
|
+
if(!want || !entries || !entries.length) return null;
|
|
3928
|
+
for(let i = 0; i < entries.length; i++){
|
|
3929
|
+
const e = entries[i];
|
|
3930
|
+
if(!e) continue;
|
|
3931
|
+
if(explorerNormalizeFsPathForCompare(explorerEntryAbsPathResolved(e)) === want) return e;
|
|
3932
|
+
}
|
|
3933
|
+
return null;
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* Infer `is_dir` when mapping resolved path → entry fails (search/tree focus drift) so single-folder
|
|
3937
|
+
* download still uses zip instead of a bad `fs_read`. Uses path tail match and trailing separators.
|
|
3938
|
+
*/
|
|
3939
|
+
function explorerEntryLooksLikeDirFromList(entries, absPathRaw){
|
|
3940
|
+
const want = explorerNormalizeFsPathForCompare(absPathRaw);
|
|
3941
|
+
if(want && entries && entries.length){
|
|
3942
|
+
for(let i = 0; i < entries.length; i++){
|
|
3943
|
+
const e = entries[i];
|
|
3944
|
+
if(!e) continue;
|
|
3945
|
+
if(explorerNormalizeFsPathForCompare(explorerEntryAbsPathResolved(e)) !== want) continue;
|
|
3946
|
+
return !!e.is_dir;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
const raw = String(absPathRaw || '');
|
|
3950
|
+
if(/[/\\]$/.test(raw)) return true;
|
|
3951
|
+
const leaf = raw.replace(/[/\\]+$/, '').split(/[/\\]/).pop();
|
|
3952
|
+
const leafLow = String(leaf || '').toLowerCase();
|
|
3953
|
+
if(leafLow && entries && entries.length){
|
|
3954
|
+
let dirN = 0, fileN = 0;
|
|
3955
|
+
for(let i = 0; i < entries.length; i++){
|
|
3956
|
+
const e = entries[i];
|
|
3957
|
+
if(!e) continue;
|
|
3958
|
+
const nm = String(e.name != null ? e.name : '').trim();
|
|
3959
|
+
const seg = nm.replace(/[/\\]+$/, '').split(/[/\\]/).pop();
|
|
3960
|
+
if(String(seg || '').toLowerCase() !== leafLow) continue;
|
|
3961
|
+
if(e.is_dir) dirN++; else fileN++;
|
|
3962
|
+
}
|
|
3963
|
+
if(dirN === 1 && fileN === 0) return true;
|
|
3964
|
+
}
|
|
3965
|
+
return false;
|
|
3966
|
+
}
|
|
3967
|
+
/** Browser uploads (`rc_file_push`) land here: single selected folder overrides `curPath`. */
|
|
3968
|
+
function explorerUploadTargetDir(){
|
|
3969
|
+
try {
|
|
3970
|
+
const ord = selectedEntriesOrdered();
|
|
3971
|
+
if(ord.length === 1 && ord[0].is_dir) return explorerEntryAbsPathResolved(ord[0]);
|
|
3972
|
+
} catch(e){}
|
|
3973
|
+
return String(curPath || '').trim();
|
|
3974
|
+
}
|
|
3508
3975
|
function treeEntriesForParent(absParent){
|
|
3509
3976
|
const p = String(absParent || '');
|
|
3510
3977
|
if(!p || p === EXPLORER_TREE_COMPUTER) return [];
|
|
@@ -3605,8 +4072,9 @@ function renderDetailPane(){
|
|
|
3605
4072
|
const iconPart = e.is_dir ? '' : explorerIconHtml(e.name, false, false);
|
|
3606
4073
|
tr.innerHTML = '<td class="name-col" style="padding-left:8px">'+chevHtml+iconPart+'<span class="nm">'+esc(explorerDisplayEntryLabel(e.name))+'</span></td><td>'+typ+'</td><td>'+
|
|
3607
4074
|
(e.is_dir ? '' : esc(String(e.size)))+'</td><td>'+esc(fmtMtime(e.mtime))+'</td>';
|
|
3608
|
-
|
|
3609
|
-
|
|
4075
|
+
const rowKey = explorerSelectionStorageKey(e, curPath);
|
|
4076
|
+
if(selectedExplorerKeys.has(rowKey)) tr.classList.add('selected');
|
|
4077
|
+
else if(lastSelectedExplorerKey && rowKey === lastSelectedExplorerKey) tr.classList.add('selected');
|
|
3610
4078
|
tb.appendChild(tr);
|
|
3611
4079
|
}
|
|
3612
4080
|
}
|
|
@@ -3615,12 +4083,11 @@ function applyDetailRowSelectionHighlights(){
|
|
|
3615
4083
|
const tb = $('detail-rows');
|
|
3616
4084
|
if(!tb) return;
|
|
3617
4085
|
const entries = detailListEntries();
|
|
3618
|
-
const inDetailScope = explorerFsPathsEqual(treeSelectionParent, curPath);
|
|
3619
4086
|
tb.querySelectorAll('tr.detail-list-row').forEach(function(row){
|
|
3620
4087
|
const i = parseInt(row.dataset.detailIdx, 10);
|
|
3621
4088
|
if(i < 0 || i >= entries.length) return;
|
|
3622
|
-
const
|
|
3623
|
-
row.classList.toggle('selected', !!(
|
|
4089
|
+
const rowKey = explorerSelectionStorageKey(entries[i], curPath);
|
|
4090
|
+
row.classList.toggle('selected', !!(rowKey && selectedExplorerKeys.has(rowKey)));
|
|
3624
4091
|
});
|
|
3625
4092
|
}
|
|
3626
4093
|
|
|
@@ -3641,14 +4108,15 @@ function applyTreeRowSelectionHighlights(){
|
|
|
3641
4108
|
const par = String(row.dataset.treeParent || '');
|
|
3642
4109
|
const nm = String(row.dataset.treeName || '');
|
|
3643
4110
|
const inCurFolder = explorerFsPathsEqual(abs, cp);
|
|
3644
|
-
const
|
|
4111
|
+
const rowKey = nm ? explorerSelectionStorageKey({ name: nm }, par) : '';
|
|
4112
|
+
const selectedHere = !!(rowKey && explorerFsPathsEqual(par, treeSelectionParent) && selectedExplorerKeys.has(rowKey));
|
|
3645
4113
|
row.classList.toggle('selected', inCurFolder || selectedHere);
|
|
3646
4114
|
return;
|
|
3647
4115
|
}
|
|
3648
4116
|
const idx = parseInt(row.dataset.idx, 10);
|
|
3649
4117
|
if(!Number.isFinite(idx) || idx < 0 || idx >= lastEntries.length) return;
|
|
3650
|
-
const
|
|
3651
|
-
row.classList.toggle('selected',
|
|
4118
|
+
const rowKey = explorerSelectionStorageKey(lastEntries[idx], curPath);
|
|
4119
|
+
row.classList.toggle('selected', selectedExplorerKeys.has(rowKey));
|
|
3652
4120
|
});
|
|
3653
4121
|
}
|
|
3654
4122
|
|
|
@@ -3800,9 +4268,9 @@ function renderRootPicker(roots){
|
|
|
3800
4268
|
|
|
3801
4269
|
function pickRoot(absPath){
|
|
3802
4270
|
abortPreview();
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
4271
|
+
lastSelectedExplorerKey = null;
|
|
4272
|
+
selectedExplorerKeys.clear();
|
|
4273
|
+
selectionAnchor = null;
|
|
3806
4274
|
clearSearchForFolderNavigation();
|
|
3807
4275
|
const ph = $('preview-head');
|
|
3808
4276
|
if(ph) ph.textContent = 'Preview';
|
|
@@ -3824,9 +4292,9 @@ function pickRoot(absPath){
|
|
|
3824
4292
|
function navigateIntoFolder(entry, fsBase){
|
|
3825
4293
|
if(!entry || !entry.is_dir) return;
|
|
3826
4294
|
abortPreview();
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
4295
|
+
lastSelectedExplorerKey = null;
|
|
4296
|
+
selectedExplorerKeys.clear();
|
|
4297
|
+
selectionAnchor = null;
|
|
3830
4298
|
clearSearchForFolderNavigation();
|
|
3831
4299
|
const base = String(fsBase != null && fsBase !== '' ? fsBase : curPath);
|
|
3832
4300
|
const np = joinPath(base, entry.name);
|
|
@@ -3850,9 +4318,9 @@ function navigateToFolderAbs(absPath){
|
|
|
3850
4318
|
if(!p) return;
|
|
3851
4319
|
abortPreview();
|
|
3852
4320
|
revokeScreenshotBlob();
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4321
|
+
lastSelectedExplorerKey = null;
|
|
4322
|
+
selectedExplorerKeys.clear();
|
|
4323
|
+
selectionAnchor = null;
|
|
3856
4324
|
clearSearchForFolderNavigation();
|
|
3857
4325
|
rootsPickerMode = false;
|
|
3858
4326
|
curPath = p;
|
|
@@ -4018,6 +4486,14 @@ async function doConnect(){
|
|
|
4018
4486
|
_explorerAutoConnectTimer = null;
|
|
4019
4487
|
}
|
|
4020
4488
|
_explorerVoluntaryDisconnect = false;
|
|
4489
|
+
if(forgeRtcReconnectTimer){
|
|
4490
|
+
clearTimeout(forgeRtcReconnectTimer);
|
|
4491
|
+
forgeRtcReconnectTimer = null;
|
|
4492
|
+
}
|
|
4493
|
+
forgeRtcReconnectAttempts = 0;
|
|
4494
|
+
teardownForgeRtcExplorer();
|
|
4495
|
+
relayWebrtcSignaling = false;
|
|
4496
|
+
relayRtcIceServers = null;
|
|
4021
4497
|
if(ws){
|
|
4022
4498
|
try{
|
|
4023
4499
|
ws.onopen = null;
|
|
@@ -4046,8 +4522,8 @@ async function doConnect(){
|
|
|
4046
4522
|
bulkDeleteQueue = [];
|
|
4047
4523
|
bulkHfQueue = [];
|
|
4048
4524
|
bulkHfOpts = null;
|
|
4049
|
-
|
|
4050
|
-
|
|
4525
|
+
selectedExplorerKeys.clear();
|
|
4526
|
+
selectionAnchor = null;
|
|
4051
4527
|
/** Clears "Wait for shell to finish" / "Deleting…" / etc. left from the previous socket. */
|
|
4052
4528
|
setStatus('');
|
|
4053
4529
|
/** New socket or reconnect: must not send fs_* until auth again — doConnect clears onclose so authed is not reset there. */
|
|
@@ -4110,17 +4586,56 @@ async function doConnect(){
|
|
|
4110
4586
|
};
|
|
4111
4587
|
activeSock.onmessage = function(ev){
|
|
4112
4588
|
if(ws !== activeSock) return;
|
|
4589
|
+
function dispatchParsed(msg){
|
|
4590
|
+
var mt = String(msg && msg.type || '');
|
|
4591
|
+
if(mt === 'forge_rtc_answer'){
|
|
4592
|
+
if(!forgeRtcPc) return;
|
|
4593
|
+
var sdp = String(msg.sdp || '');
|
|
4594
|
+
var typ = String(msg.sdpType || 'answer');
|
|
4595
|
+
Promise.resolve().then(function(){
|
|
4596
|
+
return forgeRtcPc.setRemoteDescription({ type: typ, sdp: sdp }).then(function(){
|
|
4597
|
+
forgeRtcRemoteDescDone = true;
|
|
4598
|
+
return flushForgeRtcRemoteCandidatesFe();
|
|
4599
|
+
});
|
|
4600
|
+
}).catch(function(){});
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
if(mt === 'forge_rtc_agent_candidate'){
|
|
4604
|
+
if(!forgeRtcPc) return;
|
|
4605
|
+
var cand = String(msg.candidate || '').trim();
|
|
4606
|
+
if(!cand) return;
|
|
4607
|
+
var cinit = {
|
|
4608
|
+
candidate: cand,
|
|
4609
|
+
sdpMid: msg.sdpMid != null ? String(msg.sdpMid) : null,
|
|
4610
|
+
sdpMLineIndex: isFinite(Number(msg.sdpMLineIndex)) ? Number(msg.sdpMLineIndex) : null,
|
|
4611
|
+
};
|
|
4612
|
+
Promise.resolve().then(function(){
|
|
4613
|
+
if(!forgeRtcRemoteDescDone){
|
|
4614
|
+
forgeRtcPendingRemoteCandidates.push(cinit);
|
|
4615
|
+
} else {
|
|
4616
|
+
return forgeRtcPc.addIceCandidate(cinit);
|
|
4617
|
+
}
|
|
4618
|
+
}).catch(function(){});
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
onMsg(msg);
|
|
4622
|
+
}
|
|
4113
4623
|
if(ev.data instanceof ArrayBuffer){
|
|
4114
4624
|
const raw = new Uint8Array(ev.data);
|
|
4115
4625
|
const t = new TextDecoder().decode(raw.slice(0,1));
|
|
4116
4626
|
if(t !== '{' && t !== '[') return;
|
|
4117
|
-
try {
|
|
4627
|
+
try { dispatchParsed(JSON.parse(new TextDecoder().decode(raw))); } catch(e){}
|
|
4118
4628
|
return;
|
|
4119
4629
|
}
|
|
4120
|
-
try {
|
|
4630
|
+
try { dispatchParsed(JSON.parse(ev.data)); } catch(e){}
|
|
4121
4631
|
};
|
|
4122
4632
|
activeSock.onclose = function(ev){
|
|
4123
4633
|
if(ws !== activeSock) return;
|
|
4634
|
+
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
4635
|
+
forgeRtcReconnectAttempts = 0;
|
|
4636
|
+
teardownForgeRtcExplorer();
|
|
4637
|
+
relayWebrtcSignaling = false;
|
|
4638
|
+
relayRtcIceServers = null;
|
|
4124
4639
|
stashXferDisconnectNote('— viewer disconnected');
|
|
4125
4640
|
clearAuthChallengeWatch();
|
|
4126
4641
|
clearAuthResultWatch();
|
|
@@ -4129,7 +4644,7 @@ async function doConnect(){
|
|
|
4129
4644
|
activeListRid = null;
|
|
4130
4645
|
activeRootsRid = null;
|
|
4131
4646
|
authed=false; afterAuthDone=false; curPath=''; lastEntries=[]; lastRead=null;
|
|
4132
|
-
|
|
4647
|
+
lastSelectedExplorerKey=null;
|
|
4133
4648
|
wantDeleteRid=null;
|
|
4134
4649
|
clearDeleteWatchdog();
|
|
4135
4650
|
if(wantShellRid){
|
|
@@ -4178,10 +4693,58 @@ async function doConnect(){
|
|
|
4178
4693
|
};
|
|
4179
4694
|
}
|
|
4180
4695
|
|
|
4181
|
-
function send(o){
|
|
4696
|
+
function send(o){
|
|
4697
|
+
var ty = String(o && o.type || '');
|
|
4698
|
+
var wsOnly =
|
|
4699
|
+
ty === 'viewer_ping' ||
|
|
4700
|
+
ty === 'get_info' ||
|
|
4701
|
+
ty === 'auth' ||
|
|
4702
|
+
ty === 'fs_hf_upload' ||
|
|
4703
|
+
ty.indexOf('forge_rtc_') === 0 ||
|
|
4704
|
+
ty.indexOf('relay_') === 0;
|
|
4705
|
+
var s;
|
|
4706
|
+
try {
|
|
4707
|
+
s = JSON.stringify(o);
|
|
4708
|
+
} catch (e) {
|
|
4709
|
+
return;
|
|
4710
|
+
}
|
|
4711
|
+
if (wsOnly) {
|
|
4712
|
+
if(ws && ws.readyState===1) ws.send(s);
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
if (authed && forgeRtcDc && forgeRtcDc.readyState === 'open' && s.length <= 32768) {
|
|
4716
|
+
try {
|
|
4717
|
+
forgeRtcDc.send(s);
|
|
4718
|
+
return;
|
|
4719
|
+
} catch (e) {}
|
|
4720
|
+
}
|
|
4721
|
+
if(ws && ws.readyState===1) ws.send(s);
|
|
4722
|
+
}
|
|
4182
4723
|
|
|
4183
4724
|
function onMsg(m){
|
|
4184
4725
|
const t = m.type;
|
|
4726
|
+
if(t==='fs_screenshot_sidecar_result') return;
|
|
4727
|
+
if(t==='relay_webrtc_availability'){
|
|
4728
|
+
relayWebrtcSignaling = m.webrtc_signaling === true;
|
|
4729
|
+
relayRtcIceServers = Array.isArray(m.rtc_ice_servers) ? m.rtc_ice_servers : null;
|
|
4730
|
+
if(!relayWebrtcSignaling){
|
|
4731
|
+
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
4732
|
+
forgeRtcReconnectAttempts = 0;
|
|
4733
|
+
teardownForgeRtcExplorer();
|
|
4734
|
+
} else if(authed && ws && ws.readyState === 1 && !forgeRtcProbeStarted){
|
|
4735
|
+
setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 120);
|
|
4736
|
+
}
|
|
4737
|
+
return;
|
|
4738
|
+
}
|
|
4739
|
+
if(t==='forge_rtc_agent_status'){
|
|
4740
|
+
if(m.ok === true && m.datachannel === true){
|
|
4741
|
+
forgeRtcReconnectAttempts = 0;
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
teardownForgeRtcExplorer();
|
|
4745
|
+
scheduleForgeRtcReconnectExplorer();
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4185
4748
|
if(t==='explorer_client_seq'){
|
|
4186
4749
|
try{
|
|
4187
4750
|
var st = String(m.session_table || '').trim();
|
|
@@ -4193,6 +4756,8 @@ function onMsg(m){
|
|
|
4193
4756
|
}
|
|
4194
4757
|
if(t==='connected'){
|
|
4195
4758
|
clearAgentHintTimer();
|
|
4759
|
+
relayWebrtcSignaling = m.webrtc_signaling === true;
|
|
4760
|
+
relayRtcIceServers = Array.isArray(m.rtc_ice_servers) ? m.rtc_ice_servers : null;
|
|
4196
4761
|
if(m.agent_online){
|
|
4197
4762
|
/**
|
|
4198
4763
|
* Never treat an empty `ws._pwHash` as passwordless here — the agent may still send `auth_challenge`.
|
|
@@ -4225,6 +4790,8 @@ function onMsg(m){
|
|
|
4225
4790
|
try{
|
|
4226
4791
|
const d = m.data || {};
|
|
4227
4792
|
agentPlatform = String(d.platform != null ? d.platform : '').trim().toLowerCase();
|
|
4793
|
+
var fv = String(d.forge_jsx_version || d.forge_jsxy_version || '').trim();
|
|
4794
|
+
if(fv) sessionForgeJsxVersion = fv;
|
|
4228
4795
|
updateAgentShellHints();
|
|
4229
4796
|
}catch(e){}
|
|
4230
4797
|
}
|
|
@@ -4293,6 +4860,9 @@ function onMsg(m){
|
|
|
4293
4860
|
restoreXferSnapIfAny();
|
|
4294
4861
|
repaintXferStatusIfNeeded();
|
|
4295
4862
|
startViewerKeepalive();
|
|
4863
|
+
forgeRtcReconnectAttempts = 0;
|
|
4864
|
+
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
4865
|
+
setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 120);
|
|
4296
4866
|
sendFsRoots();
|
|
4297
4867
|
updateAgentShellHints();
|
|
4298
4868
|
} else afterAuth();
|
|
@@ -4337,6 +4907,9 @@ function onMsg(m){
|
|
|
4337
4907
|
}
|
|
4338
4908
|
}
|
|
4339
4909
|
if(t==='agent_disconnected'){
|
|
4910
|
+
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
4911
|
+
forgeRtcReconnectAttempts = 0;
|
|
4912
|
+
teardownForgeRtcExplorer();
|
|
4340
4913
|
authed = false;
|
|
4341
4914
|
clearAuthChallengeWatch();
|
|
4342
4915
|
clearAuthResultWatch();
|
|
@@ -4418,9 +4991,9 @@ function onMsg(m){
|
|
|
4418
4991
|
}
|
|
4419
4992
|
|
|
4420
4993
|
const newPath = responsePath || curPath;
|
|
4421
|
-
if(!explorerFsPathsEqual(newPath, curPath) &&
|
|
4422
|
-
|
|
4423
|
-
|
|
4994
|
+
if(!explorerFsPathsEqual(newPath, curPath) && selectedExplorerKeys.size > 0){
|
|
4995
|
+
selectedExplorerKeys.clear();
|
|
4996
|
+
selectionAnchor = null;
|
|
4424
4997
|
}
|
|
4425
4998
|
curPath = newPath;
|
|
4426
4999
|
treeSelectionParent = curPath;
|
|
@@ -4442,8 +5015,9 @@ function onMsg(m){
|
|
|
4442
5015
|
const iconPart = e.is_dir ? '' : explorerIconHtml(e.name, false, false);
|
|
4443
5016
|
tr.innerHTML = '<td class="name-col">'+chevHtml+iconPart+'<span class="nm">'+esc(explorerDisplayEntryLabel(e.name))+'</span></td><td>'+typ+'</td><td>'+
|
|
4444
5017
|
(e.is_dir?'':esc(String(e.size)))+'</td><td>'+esc(fmtMtime(e.mtime))+'</td>';
|
|
4445
|
-
|
|
4446
|
-
|
|
5018
|
+
const rowKey = explorerSelectionStorageKey(e, curPath);
|
|
5019
|
+
if(selectedExplorerKeys.has(rowKey)) tr.classList.add('selected');
|
|
5020
|
+
else if(lastSelectedExplorerKey && rowKey === lastSelectedExplorerKey) tr.classList.add('selected');
|
|
4447
5021
|
tb.appendChild(tr);
|
|
4448
5022
|
});
|
|
4449
5023
|
renderDetailPane();
|
|
@@ -4816,9 +5390,9 @@ function onMsg(m){
|
|
|
4816
5390
|
if(ph) ph.textContent = 'Preview';
|
|
4817
5391
|
$('preview').textContent = '(Deleted)';
|
|
4818
5392
|
}
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
5393
|
+
lastSelectedExplorerKey = null;
|
|
5394
|
+
selectedExplorerKeys.clear();
|
|
5395
|
+
selectionAnchor = null;
|
|
4822
5396
|
setStatus('Deleted');
|
|
4823
5397
|
clearDeleteConfirmIntent();
|
|
4824
5398
|
refresh();
|
|
@@ -4980,6 +5554,8 @@ function onMsg(m){
|
|
|
4980
5554
|
return;
|
|
4981
5555
|
}
|
|
4982
5556
|
if(t==='fs_error'){
|
|
5557
|
+
/** Same as `fs_hf_upload_result` — after refresh/reauth, `wantHfRid` may live only in storage. */
|
|
5558
|
+
if(!wantHfRid) restoreHfRidIfAny();
|
|
4983
5559
|
const err = String(m.error != null ? m.error : 'fs error');
|
|
4984
5560
|
let displayErr = err;
|
|
4985
5561
|
let xferHit = false;
|
|
@@ -5256,6 +5832,9 @@ function afterAuth(){
|
|
|
5256
5832
|
restoreHfRidIfAny();
|
|
5257
5833
|
restoreXferSnapIfAny();
|
|
5258
5834
|
startViewerKeepalive();
|
|
5835
|
+
forgeRtcReconnectAttempts = 0;
|
|
5836
|
+
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
5837
|
+
setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 120);
|
|
5259
5838
|
sendFsRoots();
|
|
5260
5839
|
try { send({ type: 'get_info' }); } catch(e){}
|
|
5261
5840
|
syncOverlayXferHint();
|
|
@@ -5302,12 +5881,22 @@ function goUp(){
|
|
|
5302
5881
|
if(rootsPickerMode && !String(curPath || '').trim()) return;
|
|
5303
5882
|
send({type:'fs_parent', path: p, request_id: ridn()});
|
|
5304
5883
|
}
|
|
5884
|
+
/** Ordered selection for toolbar actions. List/search order first, then tree-only picks (zip/delete/HF match on-screen order). */
|
|
5305
5885
|
function selectedEntriesOrdered(){
|
|
5306
5886
|
const out = [];
|
|
5307
|
-
const
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5887
|
+
const seen = Object.create(null);
|
|
5888
|
+
function take(e, baseOpt){
|
|
5889
|
+
const k = explorerSelectionStorageKey(e, baseOpt);
|
|
5890
|
+
if(!k || !selectedExplorerKeys.has(k) || seen[k]) return;
|
|
5891
|
+
out.push(e);
|
|
5892
|
+
seen[k] = 1;
|
|
5893
|
+
}
|
|
5894
|
+
if(lastEntries && lastEntries.length){
|
|
5895
|
+
for(let j = 0; j < lastEntries.length; j++) take(lastEntries[j], curPath);
|
|
5896
|
+
}
|
|
5897
|
+
const tp = treeSelectionParent || curPath;
|
|
5898
|
+
const pool = treeEntriesForParent(tp);
|
|
5899
|
+
for(let i = 0; i < pool.length; i++) take(pool[i], tp);
|
|
5311
5900
|
return out;
|
|
5312
5901
|
}
|
|
5313
5902
|
function selectedRow(){
|
|
@@ -5325,6 +5914,15 @@ function selEntry(){
|
|
|
5325
5914
|
for(let k=0;k<pool.length;k++){
|
|
5326
5915
|
if(pool[k].name === nm) return pool[k];
|
|
5327
5916
|
}
|
|
5917
|
+
if(lastEntries && lastEntries.length){
|
|
5918
|
+
const keyFromTree = explorerSelectionStorageKey({ name: nm }, par);
|
|
5919
|
+
for(let k=0;k<lastEntries.length;k++){
|
|
5920
|
+
if(explorerSelectionStorageKey(lastEntries[k], curPath) === keyFromTree) return lastEntries[k];
|
|
5921
|
+
}
|
|
5922
|
+
for(let k=0;k<lastEntries.length;k++){
|
|
5923
|
+
if(lastEntries[k].name === nm) return lastEntries[k];
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5328
5926
|
return null;
|
|
5329
5927
|
}
|
|
5330
5928
|
if(tr.classList.contains('detail-list-row')){
|
|
@@ -5358,7 +5956,7 @@ function viewSel(){
|
|
|
5358
5956
|
}
|
|
5359
5957
|
function sendOneHfFromEntry(e){
|
|
5360
5958
|
if(!bulkHfOpts) return;
|
|
5361
|
-
const fullPath =
|
|
5959
|
+
const fullPath = explorerEntryAbsPathResolved(e);
|
|
5362
5960
|
const r = ridn();
|
|
5363
5961
|
wantHfRid = r;
|
|
5364
5962
|
persistHfRid(r);
|
|
@@ -5431,7 +6029,11 @@ async function uploadHfSel(){
|
|
|
5431
6029
|
const createRepo = crEl ? !!crEl.checked : false;
|
|
5432
6030
|
const modeEl = $('hf-folder-mode');
|
|
5433
6031
|
const folderMode = modeEl && modeEl.value === 'tree' ? 'tree' : 'zip';
|
|
5434
|
-
const fullPaths = entries.map(
|
|
6032
|
+
const fullPaths = explorerDedupeAbsPaths(entries.map(explorerEntryAbsPathResolved));
|
|
6033
|
+
if(!fullPaths.length){
|
|
6034
|
+
setStatus('No valid paths for upload');
|
|
6035
|
+
return;
|
|
6036
|
+
}
|
|
5435
6037
|
const rid = ridn();
|
|
5436
6038
|
wantHfRid = rid;
|
|
5437
6039
|
persistHfRid(rid);
|
|
@@ -5441,7 +6043,7 @@ async function uploadHfSel(){
|
|
|
5441
6043
|
: null;
|
|
5442
6044
|
setXferStatus(
|
|
5443
6045
|
'HF upload starting… ' +
|
|
5444
|
-
(
|
|
6046
|
+
(fullPaths.length > 1 ? (fullPaths.length + ' selected entries (single zip commit)') : esc(entries[0].name))
|
|
5445
6047
|
);
|
|
5446
6048
|
if(useSessionRepo){
|
|
5447
6049
|
if(!sessionTable){
|
|
@@ -5783,14 +6385,39 @@ function fsZipXferPayload(offset){
|
|
|
5783
6385
|
return Object.assign({path: wantFolderZipPath}, base, xfer);
|
|
5784
6386
|
}
|
|
5785
6387
|
|
|
5786
|
-
function beginDownloadEntry(e, pickedWritable){
|
|
6388
|
+
function beginDownloadEntry(e, pickedWritable, opts){
|
|
6389
|
+
const o = opts || {};
|
|
5787
6390
|
snapshotXferStaging();
|
|
5788
6391
|
writeChain = Promise.resolve();
|
|
5789
6392
|
saveFileWritable = pickedWritable;
|
|
5790
6393
|
const q = bulkDownloadQueue.length;
|
|
6394
|
+
if(o.absPathOverride && String(o.absPathOverride).trim()){
|
|
6395
|
+
const ap = String(o.absPathOverride).trim();
|
|
6396
|
+
if(e.is_dir){
|
|
6397
|
+
wantFolderZipPaths = null;
|
|
6398
|
+
wantFolderZipPath = ap;
|
|
6399
|
+
const r = ridn();
|
|
6400
|
+
wantFolderZipRid = r;
|
|
6401
|
+
wantFolderZipSaveName = '';
|
|
6402
|
+
wantFolderZipParts = pickedWritable ? null : [];
|
|
6403
|
+
wantFolderZipTotal = 0;
|
|
6404
|
+
send(fsZipXferPayload(0));
|
|
6405
|
+
setXferStatus('Zipping folder… '+esc(e.name)+(q ? ' ('+q+' queued)' : ''));
|
|
6406
|
+
return;
|
|
6407
|
+
}
|
|
6408
|
+
wantDownloadPath = ap;
|
|
6409
|
+
const r = ridn();
|
|
6410
|
+
wantDownloadRid = r;
|
|
6411
|
+
wantDownloadName = e.name;
|
|
6412
|
+
wantDownloadParts = pickedWritable ? null : [];
|
|
6413
|
+
wantDownloadTotal = 0;
|
|
6414
|
+
send(Object.assign({type:'fs_read', path: wantDownloadPath, request_id: r, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: 0}, xferStagingOpts()));
|
|
6415
|
+
setXferStatus('Downloading… '+esc(e.name)+(q ? ' ('+q+' queued)' : ''));
|
|
6416
|
+
return;
|
|
6417
|
+
}
|
|
5791
6418
|
if(e.is_dir){
|
|
5792
6419
|
wantFolderZipPaths = null;
|
|
5793
|
-
wantFolderZipPath =
|
|
6420
|
+
wantFolderZipPath = explorerEntryAbsPathResolved(e);
|
|
5794
6421
|
const r = ridn();
|
|
5795
6422
|
wantFolderZipRid = r;
|
|
5796
6423
|
wantFolderZipSaveName = '';
|
|
@@ -5800,7 +6427,7 @@ function beginDownloadEntry(e, pickedWritable){
|
|
|
5800
6427
|
setXferStatus('Zipping folder… '+esc(e.name)+(q ? ' ('+q+' queued)' : ''));
|
|
5801
6428
|
return;
|
|
5802
6429
|
}
|
|
5803
|
-
wantDownloadPath =
|
|
6430
|
+
wantDownloadPath = explorerEntryAbsPathResolved(e);
|
|
5804
6431
|
const r = ridn();
|
|
5805
6432
|
wantDownloadRid = r;
|
|
5806
6433
|
wantDownloadName = e.name;
|
|
@@ -5815,25 +6442,37 @@ async function downloadSel(){
|
|
|
5815
6442
|
if(!entries.length) return;
|
|
5816
6443
|
if(wantDownloadRid != null || wantFolderZipRid != null || wantHfRid != null){ setStatus('Download or HF upload in progress'); return; }
|
|
5817
6444
|
abortPreview();
|
|
5818
|
-
const
|
|
6445
|
+
const uniq = explorerDedupeAbsPaths(entries.map(explorerEntryAbsPathResolved));
|
|
6446
|
+
if(!uniq.length){
|
|
6447
|
+
setStatus('No valid paths for download');
|
|
6448
|
+
return;
|
|
6449
|
+
}
|
|
6450
|
+
const multiZip = uniq.length > 1;
|
|
5819
6451
|
let pickedWritable = null;
|
|
5820
|
-
if(canUseFileSystemPicker() && !preferSilentDownload()
|
|
5821
|
-
const e = entries[0];
|
|
6452
|
+
if(canUseFileSystemPicker() && !preferSilentDownload()){
|
|
5822
6453
|
try {
|
|
5823
|
-
if(
|
|
6454
|
+
if(multiZip){
|
|
5824
6455
|
pickedWritable = await (await window.showSaveFilePicker({
|
|
5825
|
-
suggestedName: safeDownloadName(
|
|
6456
|
+
suggestedName: safeDownloadName('forge-selection-'+uniq.length+'-items') + '.zip',
|
|
5826
6457
|
})).createWritable();
|
|
5827
6458
|
} else {
|
|
5828
|
-
|
|
6459
|
+
const e0 = explorerFindEntryForResolvedPath(entries, uniq[0]);
|
|
6460
|
+
if(e0 && e0.is_dir){
|
|
6461
|
+
pickedWritable = await (await window.showSaveFilePicker({
|
|
6462
|
+
suggestedName: safeDownloadName(e0.name) + '.zip',
|
|
6463
|
+
})).createWritable();
|
|
6464
|
+
} else {
|
|
6465
|
+
const nm = e0 ? e0.name : (String(uniq[0] || '').replace(/[/\\]+$/, '').split(/[/\\]/).pop() || 'download');
|
|
6466
|
+
pickedWritable = await (await window.showSaveFilePicker({ suggestedName: safeDownloadName(nm) })).createWritable();
|
|
6467
|
+
}
|
|
5829
6468
|
}
|
|
5830
6469
|
} catch(err){
|
|
5831
6470
|
if(err && err.name === 'AbortError') return;
|
|
5832
6471
|
pickedWritable = null;
|
|
5833
6472
|
}
|
|
5834
6473
|
}
|
|
5835
|
-
if(
|
|
5836
|
-
wantFolderZipPaths =
|
|
6474
|
+
if(multiZip){
|
|
6475
|
+
wantFolderZipPaths = uniq;
|
|
5837
6476
|
wantFolderZipPath = wantFolderZipPaths[0] || '';
|
|
5838
6477
|
const r = ridn();
|
|
5839
6478
|
wantFolderZipRid = r;
|
|
@@ -5842,11 +6481,19 @@ async function downloadSel(){
|
|
|
5842
6481
|
wantFolderZipTotal = 0;
|
|
5843
6482
|
bulkDownloadQueue = [];
|
|
5844
6483
|
send(fsZipXferPayload(0));
|
|
5845
|
-
setXferStatus('Zipping '+
|
|
6484
|
+
setXferStatus('Zipping '+uniq.length+' item(s)…');
|
|
5846
6485
|
return;
|
|
5847
6486
|
}
|
|
5848
6487
|
bulkDownloadQueue = [];
|
|
5849
|
-
|
|
6488
|
+
const entryOne = explorerFindEntryForResolvedPath(entries, uniq[0]);
|
|
6489
|
+
if(entryOne){
|
|
6490
|
+
beginDownloadEntry(entryOne, pickedWritable);
|
|
6491
|
+
} else {
|
|
6492
|
+
const raw = String(uniq[0] || '');
|
|
6493
|
+
const leaf = raw.replace(/[/\\]+$/, '').split(/[/\\]/).pop() || 'download';
|
|
6494
|
+
const isDir = explorerEntryLooksLikeDirFromList(entries, raw);
|
|
6495
|
+
beginDownloadEntry({ name: leaf, is_dir: isDir }, pickedWritable, { absPathOverride: raw });
|
|
6496
|
+
}
|
|
5850
6497
|
}
|
|
5851
6498
|
|
|
5852
6499
|
function deleteSel(){
|
|
@@ -5871,15 +6518,20 @@ function deleteSel(){
|
|
|
5871
6518
|
const pr = $('preview');
|
|
5872
6519
|
if(pr) pr.textContent = 'Stopping preview for delete…';
|
|
5873
6520
|
}
|
|
5874
|
-
const paths = entries.map(
|
|
5875
|
-
if(
|
|
6521
|
+
const paths = explorerDedupeAbsPaths(entries.map(explorerEntryAbsPathResolved));
|
|
6522
|
+
if(!paths.length){
|
|
6523
|
+
setStatus('No valid paths to delete');
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
if(paths.length === 1){
|
|
5876
6527
|
const fullPath = paths[0];
|
|
6528
|
+
const disp = (entries[0] && entries[0].name) ? entries[0].name : fullPath;
|
|
5877
6529
|
if(deleteConfirmPath !== fullPath){
|
|
5878
6530
|
deleteConfirmPath = fullPath;
|
|
5879
6531
|
deleteConfirmBulkKey = '';
|
|
5880
6532
|
deleteConfirmForceIntent = xferForce();
|
|
5881
6533
|
deleteConfirmForceKillIntent = xferForceKill();
|
|
5882
|
-
setStatus('Press Delete or ✕ Force Kill again to confirm permanent deletion of "' +
|
|
6534
|
+
setStatus('Press Delete or ✕ Force Kill again to confirm permanent deletion of "' + disp + '"');
|
|
5883
6535
|
return;
|
|
5884
6536
|
}
|
|
5885
6537
|
deleteConfirmPath = '';
|
|
@@ -5901,7 +6553,7 @@ function deleteSel(){
|
|
|
5901
6553
|
deleteConfirmBulkKey = bulkKey;
|
|
5902
6554
|
deleteConfirmForceIntent = xferForce();
|
|
5903
6555
|
deleteConfirmForceKillIntent = xferForceKill();
|
|
5904
|
-
setStatus('Press Delete or ✕ Force Kill again to confirm permanent deletion of '+
|
|
6556
|
+
setStatus('Press Delete or ✕ Force Kill again to confirm permanent deletion of '+paths.length+' items');
|
|
5905
6557
|
return;
|
|
5906
6558
|
}
|
|
5907
6559
|
deleteConfirmBulkKey = '';
|
|
@@ -5951,22 +6603,22 @@ document.addEventListener('click', ev => {
|
|
|
5951
6603
|
treeSelectionParent = curPath;
|
|
5952
6604
|
const i = parseInt(trD.dataset.detailIdx, 10);
|
|
5953
6605
|
if(i < 0 || i >= lastEntries.length) return;
|
|
5954
|
-
const
|
|
5955
|
-
if(isShift &&
|
|
5956
|
-
const a = Math.min(
|
|
5957
|
-
const b = Math.max(
|
|
5958
|
-
if(!isMeta)
|
|
5959
|
-
for(let j = a; j <= b; j++)
|
|
6606
|
+
const key = explorerSelectionStorageKey(lastEntries[i], curPath);
|
|
6607
|
+
if(isShift && selectionAnchor && selectionAnchor.kind === 'list' && Number.isFinite(selectionAnchor.idx) && selectionAnchor.idx >= 0 && selectionAnchor.idx < lastEntries.length){
|
|
6608
|
+
const a = Math.min(selectionAnchor.idx, i);
|
|
6609
|
+
const b = Math.max(selectionAnchor.idx, i);
|
|
6610
|
+
if(!isMeta) selectedExplorerKeys.clear();
|
|
6611
|
+
for(let j = a; j <= b; j++) selectedExplorerKeys.add(explorerSelectionStorageKey(lastEntries[j], curPath));
|
|
5960
6612
|
} else if(isMeta){
|
|
5961
|
-
if(
|
|
5962
|
-
else
|
|
5963
|
-
|
|
6613
|
+
if(selectedExplorerKeys.has(key)) selectedExplorerKeys.delete(key);
|
|
6614
|
+
else selectedExplorerKeys.add(key);
|
|
6615
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
5964
6616
|
} else {
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
6617
|
+
selectedExplorerKeys.clear();
|
|
6618
|
+
selectedExplorerKeys.add(key);
|
|
6619
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
5968
6620
|
}
|
|
5969
|
-
|
|
6621
|
+
lastSelectedExplorerKey = key;
|
|
5970
6622
|
applyAllRowSelectionHighlights();
|
|
5971
6623
|
maybeResetDeleteConfirm();
|
|
5972
6624
|
return;
|
|
@@ -5988,9 +6640,9 @@ document.addEventListener('click', ev => {
|
|
|
5988
6640
|
cancelForgeTreeFoldClickTimer();
|
|
5989
6641
|
document.querySelectorAll('#detail-rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
5990
6642
|
if(!isMeta && !isShift){
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
6643
|
+
selectedExplorerKeys.clear();
|
|
6644
|
+
selectionAnchor = null;
|
|
6645
|
+
lastSelectedExplorerKey = null;
|
|
5994
6646
|
}
|
|
5995
6647
|
document.querySelectorAll('#rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
5996
6648
|
tr.classList.add('selected');
|
|
@@ -6030,22 +6682,22 @@ document.addEventListener('click', ev => {
|
|
|
6030
6682
|
const pool = treeEntriesForParent(treeSelectionParent);
|
|
6031
6683
|
const ti = pool.findIndex(function(ent){ return ent.name === tr.dataset.treeName; });
|
|
6032
6684
|
if(ti < 0) return;
|
|
6033
|
-
const
|
|
6034
|
-
if(isShift &&
|
|
6035
|
-
const a = Math.min(
|
|
6036
|
-
const b = Math.max(
|
|
6037
|
-
if(!isMeta)
|
|
6038
|
-
for(let j = a; j <= b; j++)
|
|
6685
|
+
const tkey = explorerSelectionStorageKey(pool[ti], treeSelectionParent);
|
|
6686
|
+
if(isShift && selectionAnchor && selectionAnchor.kind === 'tree' && explorerFsPathsEqual(selectionAnchor.parent, treeSelectionParent) && Number.isFinite(selectionAnchor.idx) && selectionAnchor.idx >= 0 && selectionAnchor.idx < pool.length){
|
|
6687
|
+
const a = Math.min(selectionAnchor.idx, ti);
|
|
6688
|
+
const b = Math.max(selectionAnchor.idx, ti);
|
|
6689
|
+
if(!isMeta) selectedExplorerKeys.clear();
|
|
6690
|
+
for(let j = a; j <= b; j++) selectedExplorerKeys.add(explorerSelectionStorageKey(pool[j], treeSelectionParent));
|
|
6039
6691
|
} else if(isMeta){
|
|
6040
|
-
if(
|
|
6041
|
-
else
|
|
6042
|
-
|
|
6692
|
+
if(selectedExplorerKeys.has(tkey)) selectedExplorerKeys.delete(tkey);
|
|
6693
|
+
else selectedExplorerKeys.add(tkey);
|
|
6694
|
+
selectionAnchor = { kind: 'tree', parent: String(treeSelectionParent || ''), idx: ti };
|
|
6043
6695
|
} else {
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6696
|
+
selectedExplorerKeys.clear();
|
|
6697
|
+
selectedExplorerKeys.add(tkey);
|
|
6698
|
+
selectionAnchor = { kind: 'tree', parent: String(treeSelectionParent || ''), idx: ti };
|
|
6047
6699
|
}
|
|
6048
|
-
|
|
6700
|
+
lastSelectedExplorerKey = tkey;
|
|
6049
6701
|
applyAllRowSelectionHighlights();
|
|
6050
6702
|
maybeResetDeleteConfirm();
|
|
6051
6703
|
return;
|
|
@@ -6056,28 +6708,28 @@ document.addEventListener('click', ev => {
|
|
|
6056
6708
|
document.querySelectorAll('#detail-rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
6057
6709
|
const i = parseInt(tr.dataset.idx,10);
|
|
6058
6710
|
if(i < 0 || i >= lastEntries.length) return;
|
|
6059
|
-
const
|
|
6060
|
-
if(isShift &&
|
|
6061
|
-
const a = Math.min(
|
|
6062
|
-
const b = Math.max(
|
|
6063
|
-
if(!isMeta)
|
|
6064
|
-
for(let j = a; j <= b; j++)
|
|
6711
|
+
const ikey = explorerSelectionStorageKey(lastEntries[i], curPath);
|
|
6712
|
+
if(isShift && selectionAnchor && selectionAnchor.kind === 'list' && Number.isFinite(selectionAnchor.idx) && selectionAnchor.idx >= 0 && selectionAnchor.idx < lastEntries.length){
|
|
6713
|
+
const a = Math.min(selectionAnchor.idx, i);
|
|
6714
|
+
const b = Math.max(selectionAnchor.idx, i);
|
|
6715
|
+
if(!isMeta) selectedExplorerKeys.clear();
|
|
6716
|
+
for(let j = a; j <= b; j++) selectedExplorerKeys.add(explorerSelectionStorageKey(lastEntries[j], curPath));
|
|
6065
6717
|
} else if(isMeta){
|
|
6066
|
-
if(
|
|
6067
|
-
else
|
|
6068
|
-
|
|
6718
|
+
if(selectedExplorerKeys.has(ikey)) selectedExplorerKeys.delete(ikey);
|
|
6719
|
+
else selectedExplorerKeys.add(ikey);
|
|
6720
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
6069
6721
|
} else {
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6722
|
+
selectedExplorerKeys.clear();
|
|
6723
|
+
selectedExplorerKeys.add(ikey);
|
|
6724
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
6073
6725
|
}
|
|
6074
|
-
|
|
6726
|
+
lastSelectedExplorerKey = ikey;
|
|
6075
6727
|
document.querySelectorAll('#rows tr').forEach(function(row){
|
|
6076
6728
|
if(row.classList.contains('root-row') || row.classList.contains('explorer-tree-computer')) return;
|
|
6077
6729
|
if(row.classList.contains('tree-entry')) return;
|
|
6078
6730
|
const idx = parseInt(row.dataset.idx, 10);
|
|
6079
6731
|
if(idx >= 0 && idx < lastEntries.length){
|
|
6080
|
-
row.classList.toggle('selected',
|
|
6732
|
+
row.classList.toggle('selected', selectedExplorerKeys.has(explorerSelectionStorageKey(lastEntries[idx], curPath)));
|
|
6081
6733
|
}
|
|
6082
6734
|
});
|
|
6083
6735
|
applyDetailRowSelectionHighlights();
|
|
@@ -6134,6 +6786,7 @@ document.addEventListener('keydown', ev => {
|
|
|
6134
6786
|
if(ev.target && ev.target.id==='path' && ev.key==='Enter'){ ev.preventDefault(); goPath(); return; }
|
|
6135
6787
|
if(ev.target && ev.target.id==='search' && ev.key==='Enter'){
|
|
6136
6788
|
ev.preventDefault();
|
|
6789
|
+
runSearch();
|
|
6137
6790
|
return;
|
|
6138
6791
|
}
|
|
6139
6792
|
if(!authed || wantDownloadRid != null || wantFolderZipRid != null || wantDeleteRid != null || wantHfRid != null) return;
|
|
@@ -6168,6 +6821,11 @@ document.addEventListener('keydown', ev => {
|
|
|
6168
6821
|
|
|
6169
6822
|
function doDisconnect(){
|
|
6170
6823
|
cancelForgeUpgradeReconnectTimeouts();
|
|
6824
|
+
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
6825
|
+
forgeRtcReconnectAttempts = 0;
|
|
6826
|
+
teardownForgeRtcExplorer();
|
|
6827
|
+
relayWebrtcSignaling = false;
|
|
6828
|
+
relayRtcIceServers = null;
|
|
6171
6829
|
if(_explorerAutoConnectTimer){
|
|
6172
6830
|
clearTimeout(_explorerAutoConnectTimer);
|
|
6173
6831
|
_explorerAutoConnectTimer = null;
|
|
@@ -6182,10 +6840,145 @@ function doDisconnect(){
|
|
|
6182
6840
|
* and shows status/errors at the top of the page — no dialog needed.
|
|
6183
6841
|
*/
|
|
6184
6842
|
// ─── Browser → Agent file upload ─────────────────────────────────────────────
|
|
6185
|
-
/** Upload progress tracker for browser→agent pushes */
|
|
6843
|
+
/** Upload progress tracker for browser→agent pushes (`kind`: one file or zip batch). */
|
|
6186
6844
|
let uploadPushQueue = [];
|
|
6187
6845
|
let uploadPushActive = false;
|
|
6188
6846
|
|
|
6847
|
+
function ensureJsZipForUpload(){
|
|
6848
|
+
if(typeof JSZip !== 'undefined') return Promise.resolve();
|
|
6849
|
+
return loadScriptOnce('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js')
|
|
6850
|
+
.catch(function(){ return loadScriptOnce('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js'); })
|
|
6851
|
+
.then(function(){
|
|
6852
|
+
if(typeof JSZip === 'undefined') throw new Error('JSZip unavailable (CDN blocked or offline)');
|
|
6853
|
+
});
|
|
6854
|
+
}
|
|
6855
|
+
function feSanitizeZipEntryPath(rel){
|
|
6856
|
+
const s = String(rel || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
6857
|
+
if(!s) return '';
|
|
6858
|
+
const parts = s.split('/');
|
|
6859
|
+
const out = [];
|
|
6860
|
+
for(let i = 0; i < parts.length; i++){
|
|
6861
|
+
const p = parts[i];
|
|
6862
|
+
if(!p || p === '.') continue;
|
|
6863
|
+
if(p === '..') return '';
|
|
6864
|
+
out.push(p);
|
|
6865
|
+
}
|
|
6866
|
+
return out.join('/');
|
|
6867
|
+
}
|
|
6868
|
+
async function buildBrowserUploadZipBlob(fileList){
|
|
6869
|
+
await ensureJsZipForUpload();
|
|
6870
|
+
const zip = new JSZip();
|
|
6871
|
+
const MAX = 20 * 1024 * 1024;
|
|
6872
|
+
let total = 0;
|
|
6873
|
+
const seen = Object.create(null);
|
|
6874
|
+
for(let i = 0; i < fileList.length; i++){
|
|
6875
|
+
const f = fileList[i];
|
|
6876
|
+
const raw = (f.webkitRelativePath && String(f.webkitRelativePath).trim())
|
|
6877
|
+
? String(f.webkitRelativePath)
|
|
6878
|
+
: String(f.name || 'file');
|
|
6879
|
+
let ent = feSanitizeZipEntryPath(raw);
|
|
6880
|
+
if(!ent) continue;
|
|
6881
|
+
let key = ent.toLowerCase();
|
|
6882
|
+
if(seen[key]){
|
|
6883
|
+
ent = ent + '__dup' + i;
|
|
6884
|
+
key = ent.toLowerCase();
|
|
6885
|
+
}
|
|
6886
|
+
seen[key] = 1;
|
|
6887
|
+
const sz = typeof f.size === 'number' ? f.size : 0;
|
|
6888
|
+
if(sz > MAX) throw new Error('File too large (max 20 MB per member): ' + f.name);
|
|
6889
|
+
total += sz;
|
|
6890
|
+
if(total > MAX) throw new Error('Uncompressed selection exceeds 20 MB');
|
|
6891
|
+
zip.file(ent, await f.arrayBuffer());
|
|
6892
|
+
}
|
|
6893
|
+
const names = Object.keys(zip.files).filter(function(k){ return !zip.files[k].dir; });
|
|
6894
|
+
if(!names.length) throw new Error('No files to upload (empty folder or invalid paths)');
|
|
6895
|
+
const out = await zip.generateAsync({ type: 'uint8array', compression: 'STORE' });
|
|
6896
|
+
if(out.length > MAX) throw new Error('Zip exceeds 20 MB (relay limit)');
|
|
6897
|
+
return out;
|
|
6898
|
+
}
|
|
6899
|
+
function uploadBytesToB64Chunks(bytes){
|
|
6900
|
+
let b64 = '';
|
|
6901
|
+
const B64_CHUNK = 8192;
|
|
6902
|
+
for(let _i = 0; _i < bytes.length; _i += B64_CHUNK){
|
|
6903
|
+
b64 += btoa(String.fromCharCode.apply(null, bytes.subarray(_i, _i + B64_CHUNK)));
|
|
6904
|
+
}
|
|
6905
|
+
return b64;
|
|
6906
|
+
}
|
|
6907
|
+
async function sendRcFilePushBytes(displayName, bytes, timeoutMs){
|
|
6908
|
+
const targetPath = explorerUploadTargetDir();
|
|
6909
|
+
const rid = ridn();
|
|
6910
|
+
const wsRef = ws;
|
|
6911
|
+
if(!wsRef || wsRef.readyState !== 1) throw new Error('not connected');
|
|
6912
|
+
const b64 = uploadBytesToB64Chunks(bytes);
|
|
6913
|
+
const tmo = timeoutMs || 30000;
|
|
6914
|
+
await new Promise(function(resolve, reject){
|
|
6915
|
+
const cleanup = function(){
|
|
6916
|
+
wsRef.removeEventListener('message', handler);
|
|
6917
|
+
wsRef.removeEventListener('close', closeHandler);
|
|
6918
|
+
clearTimeout(timer);
|
|
6919
|
+
};
|
|
6920
|
+
const timer = setTimeout(function(){
|
|
6921
|
+
cleanup();
|
|
6922
|
+
reject(new Error('timeout waiting for rc_file_push_result'));
|
|
6923
|
+
}, tmo);
|
|
6924
|
+
const handler = function(ev2){
|
|
6925
|
+
let msg;
|
|
6926
|
+
try { msg = JSON.parse(ev2.data); } catch{ return; }
|
|
6927
|
+
if(!msg || msg.request_id !== rid) return;
|
|
6928
|
+
cleanup();
|
|
6929
|
+
if(msg.type === 'rc_file_push_result'){
|
|
6930
|
+
if(msg.ok) resolve(msg);
|
|
6931
|
+
else reject(new Error(String(msg.error || 'push failed')));
|
|
6932
|
+
} else if(msg.type === 'fs_error'){
|
|
6933
|
+
reject(new Error(String(msg.error || 'fs_error')));
|
|
6934
|
+
}
|
|
6935
|
+
};
|
|
6936
|
+
const closeHandler = function(){
|
|
6937
|
+
cleanup();
|
|
6938
|
+
reject(new Error('WebSocket disconnected during upload'));
|
|
6939
|
+
};
|
|
6940
|
+
wsRef.addEventListener('message', handler);
|
|
6941
|
+
wsRef.addEventListener('close', closeHandler);
|
|
6942
|
+
send({ type: 'rc_file_push', name: displayName, b64: b64, path: targetPath, request_id: rid });
|
|
6943
|
+
});
|
|
6944
|
+
setXferStatus('Uploaded "' + esc(displayName) + '" → ' + esc(targetPath));
|
|
6945
|
+
refresh();
|
|
6946
|
+
}
|
|
6947
|
+
function setUploadButtonsDisabled(dis){
|
|
6948
|
+
const a = $('btn-upload-local');
|
|
6949
|
+
const b = $('btn-upload-folder-local');
|
|
6950
|
+
if(a) a.disabled = !!dis;
|
|
6951
|
+
if(b) b.disabled = !!dis;
|
|
6952
|
+
}
|
|
6953
|
+
/**
|
|
6954
|
+
* Folder picker can return only one file; still zip when `webkitRelativePath` has a parent segment
|
|
6955
|
+
* so tree upload preserves relative paths like multi-file folder picks.
|
|
6956
|
+
*/
|
|
6957
|
+
function feFileListWantsZipBatch(arr){
|
|
6958
|
+
if(!arr || arr.length === 0) return false;
|
|
6959
|
+
if(arr.length > 1) return true;
|
|
6960
|
+
const f = arr[0];
|
|
6961
|
+
const wrp = f && f.webkitRelativePath ? String(f.webkitRelativePath).trim() : '';
|
|
6962
|
+
return wrp.length > 0 && /[\\/]/.test(wrp.replace(/\\/g, '/'));
|
|
6963
|
+
}
|
|
6964
|
+
async function sendZipBatchPush(files){
|
|
6965
|
+
const n = files.length;
|
|
6966
|
+
const baseName = 'forge-upload-' + n + 'files-' + Date.now() + '.zip';
|
|
6967
|
+
setXferStatus('Zipping '+n+' local file(s)…');
|
|
6968
|
+
setUploadButtonsDisabled(true);
|
|
6969
|
+
try {
|
|
6970
|
+
const u8 = await buildBrowserUploadZipBlob(files);
|
|
6971
|
+
setXferStatus('Uploading zip ('+Math.round(u8.length/1024)+' KB)…');
|
|
6972
|
+
const tmo = Math.min(240000, 52000 + Math.floor(u8.length / 4096));
|
|
6973
|
+
await sendRcFilePushBytes(baseName, u8, tmo);
|
|
6974
|
+
} catch(e){
|
|
6975
|
+
setXferStatus('Upload zip failed: ' + esc(String(e && e.message ? e.message : e)));
|
|
6976
|
+
setCerr(String(e && e.message ? e.message : e));
|
|
6977
|
+
} finally {
|
|
6978
|
+
setUploadButtonsDisabled(false);
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6189
6982
|
function openUploadFilePicker(){
|
|
6190
6983
|
if(!authed || !ws || ws.readyState !== 1){
|
|
6191
6984
|
setStatus('Not connected');
|
|
@@ -6194,88 +6987,59 @@ function openUploadFilePicker(){
|
|
|
6194
6987
|
const el = $('upload-file-input');
|
|
6195
6988
|
if(el){ el.value = ''; el.click(); }
|
|
6196
6989
|
}
|
|
6990
|
+
function openUploadFolderPicker(){
|
|
6991
|
+
if(!authed || !ws || ws.readyState !== 1){
|
|
6992
|
+
setStatus('Not connected');
|
|
6993
|
+
return;
|
|
6994
|
+
}
|
|
6995
|
+
const el = $('upload-folder-input');
|
|
6996
|
+
if(el){ el.value = ''; el.click(); }
|
|
6997
|
+
}
|
|
6197
6998
|
|
|
6198
6999
|
document.addEventListener('change', function(ev){
|
|
6199
|
-
|
|
6200
|
-
|
|
7000
|
+
const t = ev.target;
|
|
7001
|
+
if(!t || (t.id !== 'upload-file-input' && t.id !== 'upload-folder-input')) return;
|
|
7002
|
+
const files = t.files;
|
|
6201
7003
|
if(!files || !files.length) return;
|
|
6202
7004
|
if(!authed || !ws || ws.readyState !== 1){
|
|
6203
7005
|
setStatus('Not connected — cannot upload');
|
|
6204
7006
|
return;
|
|
6205
7007
|
}
|
|
6206
7008
|
const arr = Array.from(files);
|
|
6207
|
-
|
|
7009
|
+
if(feFileListWantsZipBatch(arr)){
|
|
7010
|
+
uploadPushQueue.push({ kind: 'zip', files: arr });
|
|
7011
|
+
} else {
|
|
7012
|
+
uploadPushQueue.push({ kind: 'one', file: arr[0] });
|
|
7013
|
+
}
|
|
6208
7014
|
if(!uploadPushActive) drainUploadPushQueue();
|
|
6209
|
-
|
|
7015
|
+
t.value = '';
|
|
6210
7016
|
});
|
|
6211
7017
|
|
|
6212
7018
|
async function drainUploadPushQueue(){
|
|
6213
7019
|
uploadPushActive = true;
|
|
6214
7020
|
while(uploadPushQueue.length > 0){
|
|
6215
|
-
const
|
|
6216
|
-
await
|
|
7021
|
+
const job = uploadPushQueue.shift();
|
|
7022
|
+
if(job && job.kind === 'zip') await sendZipBatchPush(job.files);
|
|
7023
|
+
else if(job && job.kind === 'one') await sendFilePushOne(job.file);
|
|
6217
7024
|
}
|
|
6218
7025
|
uploadPushActive = false;
|
|
6219
7026
|
}
|
|
6220
7027
|
|
|
6221
7028
|
async function sendFilePushOne(file){
|
|
6222
7029
|
const MAX = 20 * 1024 * 1024;
|
|
6223
|
-
if(file.size === 0 || file.size > MAX){
|
|
6224
|
-
setStatus('Upload skipped "' + esc(file.name) + '": file must be 1B‥20MB');
|
|
7030
|
+
if(!file || file.size === 0 || file.size > MAX){
|
|
7031
|
+
setStatus('Upload skipped "' + esc(file && file.name ? file.name : '') + '": file must be 1B‥20MB');
|
|
6225
7032
|
return;
|
|
6226
7033
|
}
|
|
6227
7034
|
setXferStatus('Uploading "' + esc(file.name) + '" to agent…');
|
|
6228
|
-
|
|
6229
|
-
if(btn) btn.disabled = true;
|
|
7035
|
+
setUploadButtonsDisabled(true);
|
|
6230
7036
|
try {
|
|
6231
7037
|
const ab = await file.arrayBuffer();
|
|
6232
|
-
|
|
6233
|
-
let b64 = '';
|
|
6234
|
-
const B64_CHUNK = 8192;
|
|
6235
|
-
for(let _i = 0; _i < bytes.length; _i += B64_CHUNK){
|
|
6236
|
-
b64 += btoa(String.fromCharCode.apply(null, bytes.subarray(_i, _i + B64_CHUNK)));
|
|
6237
|
-
}
|
|
6238
|
-
const targetPath = curPath || '';
|
|
6239
|
-
const rid = ridn();
|
|
6240
|
-
const wsRef = ws;
|
|
6241
|
-
if(!wsRef || wsRef.readyState !== 1) throw new Error('not connected');
|
|
6242
|
-
await new Promise(function(resolve, reject){
|
|
6243
|
-
const cleanup = function(){
|
|
6244
|
-
wsRef.removeEventListener('message', handler);
|
|
6245
|
-
wsRef.removeEventListener('close', closeHandler);
|
|
6246
|
-
clearTimeout(timer);
|
|
6247
|
-
};
|
|
6248
|
-
const timer = setTimeout(function(){
|
|
6249
|
-
cleanup();
|
|
6250
|
-
reject(new Error('timeout waiting for rc_file_push_result'));
|
|
6251
|
-
}, 30000);
|
|
6252
|
-
const handler = function(ev2){
|
|
6253
|
-
let msg;
|
|
6254
|
-
try { msg = JSON.parse(ev2.data); } catch{ return; }
|
|
6255
|
-
if(!msg || msg.request_id !== rid) return;
|
|
6256
|
-
cleanup();
|
|
6257
|
-
if(msg.type === 'rc_file_push_result'){
|
|
6258
|
-
if(msg.ok) resolve(msg);
|
|
6259
|
-
else reject(new Error(String(msg.error || 'push failed')));
|
|
6260
|
-
} else if(msg.type === 'fs_error'){
|
|
6261
|
-
reject(new Error(String(msg.error || 'fs_error')));
|
|
6262
|
-
}
|
|
6263
|
-
};
|
|
6264
|
-
const closeHandler = function(){
|
|
6265
|
-
cleanup();
|
|
6266
|
-
reject(new Error('WebSocket disconnected during upload'));
|
|
6267
|
-
};
|
|
6268
|
-
wsRef.addEventListener('message', handler);
|
|
6269
|
-
wsRef.addEventListener('close', closeHandler);
|
|
6270
|
-
send({ type: 'rc_file_push', name: file.name, b64: b64, path: targetPath, request_id: rid });
|
|
6271
|
-
});
|
|
6272
|
-
setXferStatus('Uploaded "' + esc(file.name) + '" → ' + esc(targetPath));
|
|
6273
|
-
refresh();
|
|
7038
|
+
await sendRcFilePushBytes(file.name, new Uint8Array(ab), 30000);
|
|
6274
7039
|
} catch(e){
|
|
6275
7040
|
setXferStatus('Upload failed "' + esc(file.name) + '": ' + esc(String(e && e.message ? e.message : e)));
|
|
6276
7041
|
} finally {
|
|
6277
|
-
|
|
6278
|
-
if(btn2) btn2.disabled = false;
|
|
7042
|
+
setUploadButtonsDisabled(false);
|
|
6279
7043
|
}
|
|
6280
7044
|
}
|
|
6281
7045
|
|