forge-jsxy 1.0.79 → 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 +549 -199
- package/assets/remote-control-template.html +1179 -254
- package/dist/assets/files-explorer-template.html +550 -200
- package/dist/assets/remote-control-template.html +1179 -254
- package/dist/discordAgentScreenshot.js +1 -0
- package/dist/forgeBulkDc.d.ts +13 -1
- package/dist/forgeBulkDc.js +68 -24
- package/dist/fsProtocol.d.ts +9 -1
- package/dist/fsProtocol.js +256 -36
- package/dist/relayAgent.js +30 -0
- package/dist/relayServer.js +26 -0
- package/package.json +1 -1
|
@@ -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>
|
|
@@ -1731,11 +1733,12 @@ const FE_SPLIT_LS_H = 'forgeFeExplorerSplitH';
|
|
|
1731
1733
|
let currentSearchQuery = '';
|
|
1732
1734
|
/** Ignore stale fs_list/fs_roots responses when user navigates faster than the agent replies (reduces wrong UI + extra work). */
|
|
1733
1735
|
let activeListRid = null, activeRootsRid = null;
|
|
1734
|
-
/** Remember
|
|
1735
|
-
let
|
|
1736
|
-
/** Multi-select:
|
|
1737
|
-
let
|
|
1738
|
-
|
|
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;
|
|
1739
1742
|
/** Virtual root node key for expand/collapse (not a real fs path); first row label is `FE_PROJECTS_SSH_LABEL`. */
|
|
1740
1743
|
const EXPLORER_TREE_COMPUTER = '\u0000COMPUTER';
|
|
1741
1744
|
const FE_PROJECTS_SSH_LABEL = 'PROJECTS[SSH: CURSOR]';
|
|
@@ -1775,7 +1778,7 @@ const WS_CHUNK_BYTES = 23 * 4 * 1024 * 1024;
|
|
|
1775
1778
|
* steady rate so the browser, relay, and agent are less likely to spike CPU/network vs back-to-back chunks.
|
|
1776
1779
|
* Set to 0 to request the next chunk immediately (fastest / bursty).
|
|
1777
1780
|
*/
|
|
1778
|
-
const WS_CHUNK_REQUEST_GAP_MS =
|
|
1781
|
+
const WS_CHUNK_REQUEST_GAP_MS = 8;
|
|
1779
1782
|
function scheduleFsChunkRequest(fn){
|
|
1780
1783
|
if(WS_CHUNK_REQUEST_GAP_MS > 0) setTimeout(fn, WS_CHUNK_REQUEST_GAP_MS);
|
|
1781
1784
|
else fn();
|
|
@@ -2572,15 +2575,21 @@ function splitSearchQueryTokens(raw){
|
|
|
2572
2575
|
}
|
|
2573
2576
|
return out;
|
|
2574
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
|
+
}
|
|
2575
2582
|
function parseSearchTokens(q){
|
|
2576
2583
|
const norm = normalizeSearchQuery(q);
|
|
2577
2584
|
if(!norm) return [];
|
|
2578
2585
|
return splitSearchQueryTokens(norm).map(function(part){
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2586
|
+
const pRaw = String(part || '');
|
|
2587
|
+
const p = pRaw.toLowerCase();
|
|
2588
|
+
if(isGlobSearchTokenFe(pRaw) && (p.indexOf('*') >= 0 || p.indexOf('?') >= 0)){
|
|
2581
2589
|
const re = wildcardToRegex(p);
|
|
2582
|
-
return re ? { type: 'wildcard', re: re } : { type: 'contains', value: p };
|
|
2590
|
+
return re ? { type: 'wildcard', re: re, src: pRaw } : { type: 'contains', value: p };
|
|
2583
2591
|
}
|
|
2592
|
+
if(isGlobSearchTokenFe(pRaw)) return { type: 'contains', value: p };
|
|
2584
2593
|
return { type: 'contains', value: p };
|
|
2585
2594
|
});
|
|
2586
2595
|
}
|
|
@@ -2590,10 +2599,35 @@ function nameMatchesSearch(name, tokens){
|
|
|
2590
2599
|
for(let i = 0; i < tokens.length; i++){
|
|
2591
2600
|
const t = tokens[i];
|
|
2592
2601
|
if(t.type === 'contains'){
|
|
2593
|
-
if(low.indexOf(t.value)
|
|
2594
|
-
|
|
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;
|
|
2595
2612
|
}
|
|
2596
|
-
if(
|
|
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;
|
|
2629
|
+
}
|
|
2630
|
+
return false;
|
|
2597
2631
|
}
|
|
2598
2632
|
return true;
|
|
2599
2633
|
}
|
|
@@ -2611,8 +2645,8 @@ function clearSearch(){
|
|
|
2611
2645
|
if(el) el.value = '';
|
|
2612
2646
|
if(currentSearchQuery){
|
|
2613
2647
|
currentSearchQuery = '';
|
|
2614
|
-
|
|
2615
|
-
|
|
2648
|
+
selectedExplorerKeys.clear();
|
|
2649
|
+
selectionAnchor = null;
|
|
2616
2650
|
refresh();
|
|
2617
2651
|
}
|
|
2618
2652
|
}
|
|
@@ -2630,20 +2664,25 @@ function runSearch(){
|
|
|
2630
2664
|
rootsPickerMode = false;
|
|
2631
2665
|
curPath = typedPath;
|
|
2632
2666
|
treeSelectionParent = curPath;
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2667
|
+
lastSelectedExplorerKey = null;
|
|
2668
|
+
selectedExplorerKeys.clear();
|
|
2669
|
+
selectionAnchor = null;
|
|
2636
2670
|
syncPathBarDisplay();
|
|
2637
2671
|
recordNav(typedPath);
|
|
2638
2672
|
expandPathChainTo(typedPath);
|
|
2639
|
-
if(
|
|
2673
|
+
if(String(currentSearchQuery || '').trim()){
|
|
2674
|
+
lastEntries = [];
|
|
2675
|
+
renderDetailPane();
|
|
2676
|
+
} else if(treeChildrenCache.has(typedPath)) lastEntries = (treeChildrenCache.get(typedPath) || []).slice();
|
|
2640
2677
|
else lastEntries = [];
|
|
2641
2678
|
renderExplorerTree();
|
|
2642
2679
|
prefetchMissingCachesForExpanded();
|
|
2643
2680
|
}
|
|
2644
|
-
if(nextQ !== currentSearchQuery){
|
|
2645
|
-
|
|
2646
|
-
|
|
2681
|
+
if(normalizeSearchQuery(nextQ) !== normalizeSearchQuery(currentSearchQuery)){
|
|
2682
|
+
selectedExplorerKeys.clear();
|
|
2683
|
+
selectionAnchor = null;
|
|
2684
|
+
lastEntries = [];
|
|
2685
|
+
renderDetailPane();
|
|
2647
2686
|
}
|
|
2648
2687
|
currentSearchQuery = nextQ;
|
|
2649
2688
|
refresh();
|
|
@@ -2796,21 +2835,21 @@ function fePrimeExplorerCtxMenuSelection(ev){
|
|
|
2796
2835
|
if(!rowsHost || !detailHost) return;
|
|
2797
2836
|
|
|
2798
2837
|
/** Keep selection when opening the menu on any already-selected row. */
|
|
2799
|
-
function keepIfSelected(
|
|
2838
|
+
function keepIfSelected(key){ return !!(key && selectedExplorerKeys.has(key)); }
|
|
2800
2839
|
|
|
2801
2840
|
const trD = ev.target.closest('#detail-rows tr');
|
|
2802
2841
|
if(trD && detailHost.contains(trD)){
|
|
2803
2842
|
if(trD.classList.contains('fe-detail-empty')) return;
|
|
2804
2843
|
const di = parseInt(trD.dataset.detailIdx, 10);
|
|
2805
2844
|
if(di < 0 || di >= lastEntries.length) return;
|
|
2806
|
-
const
|
|
2807
|
-
if(keepIfSelected(
|
|
2845
|
+
const key = explorerSelectionStorageKey(lastEntries[di], curPath);
|
|
2846
|
+
if(keepIfSelected(key)) return;
|
|
2808
2847
|
treeSelectionParent = curPath;
|
|
2809
2848
|
document.querySelectorAll('#rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2849
|
+
selectedExplorerKeys.clear();
|
|
2850
|
+
selectedExplorerKeys.add(key);
|
|
2851
|
+
selectionAnchor = { kind: 'list', idx: di };
|
|
2852
|
+
lastSelectedExplorerKey = key;
|
|
2814
2853
|
applyAllRowSelectionHighlights();
|
|
2815
2854
|
maybeResetDeleteConfirm();
|
|
2816
2855
|
return;
|
|
@@ -2831,11 +2870,12 @@ function fePrimeExplorerCtxMenuSelection(ev){
|
|
|
2831
2870
|
var ti = pool.findIndex(function(ent){ return ent.name === tr.dataset.treeName; });
|
|
2832
2871
|
if(ti < 0) return;
|
|
2833
2872
|
var tname = pool[ti].name;
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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;
|
|
2839
2879
|
applyAllRowSelectionHighlights();
|
|
2840
2880
|
maybeResetDeleteConfirm();
|
|
2841
2881
|
return;
|
|
@@ -2844,12 +2884,12 @@ function fePrimeExplorerCtxMenuSelection(ev){
|
|
|
2844
2884
|
treeSelectionParent = curPath;
|
|
2845
2885
|
var ix = parseInt(tr.dataset.idx, 10);
|
|
2846
2886
|
if(ix < 0 || ix >= lastEntries.length) return;
|
|
2847
|
-
var
|
|
2848
|
-
if(keepIfSelected(
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
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;
|
|
2853
2893
|
applyAllRowSelectionHighlights();
|
|
2854
2894
|
maybeResetDeleteConfirm();
|
|
2855
2895
|
} catch(ex){ /* non-fatal */ }
|
|
@@ -3179,7 +3219,7 @@ function maybeResetDeleteConfirm(){
|
|
|
3179
3219
|
const ordered = selectedEntriesOrdered();
|
|
3180
3220
|
if(ordered.length <= 1){
|
|
3181
3221
|
const e = ordered[0];
|
|
3182
|
-
const p = e ?
|
|
3222
|
+
const p = e ? explorerEntryAbsPathResolved(e) : '';
|
|
3183
3223
|
if(deleteConfirmPath && p !== deleteConfirmPath){
|
|
3184
3224
|
deleteConfirmPath = '';
|
|
3185
3225
|
clearDeleteConfirmIntent();
|
|
@@ -3190,7 +3230,7 @@ function maybeResetDeleteConfirm(){
|
|
|
3190
3230
|
}
|
|
3191
3231
|
return;
|
|
3192
3232
|
}
|
|
3193
|
-
const bulkKey = ordered.map(
|
|
3233
|
+
const bulkKey = ordered.map(explorerEntryAbsPathResolved).sort().join('|');
|
|
3194
3234
|
if(deleteConfirmBulkKey && deleteConfirmBulkKey !== bulkKey){
|
|
3195
3235
|
deleteConfirmBulkKey = '';
|
|
3196
3236
|
clearDeleteConfirmIntent();
|
|
@@ -3516,9 +3556,11 @@ function startPreviewFromEntry(e, fsBase){
|
|
|
3516
3556
|
}
|
|
3517
3557
|
revokeScreenshotBlob();
|
|
3518
3558
|
abortPreview();
|
|
3519
|
-
const
|
|
3520
|
-
const
|
|
3521
|
-
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;
|
|
3522
3564
|
const ext = (name.indexOf('.') >= 0) ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '';
|
|
3523
3565
|
const size = typeof e.size === 'number' ? e.size : 0;
|
|
3524
3566
|
if(size > PREVIEW_MAX_BYTES){
|
|
@@ -3803,7 +3845,133 @@ function explorerSelectionFsBase(){
|
|
|
3803
3845
|
if(treeSelectionParent && treeSelectionParent !== EXPLORER_TREE_COMPUTER) return treeSelectionParent;
|
|
3804
3846
|
return curPath;
|
|
3805
3847
|
}
|
|
3806
|
-
|
|
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
|
+
}
|
|
3807
3975
|
function treeEntriesForParent(absParent){
|
|
3808
3976
|
const p = String(absParent || '');
|
|
3809
3977
|
if(!p || p === EXPLORER_TREE_COMPUTER) return [];
|
|
@@ -3904,8 +4072,9 @@ function renderDetailPane(){
|
|
|
3904
4072
|
const iconPart = e.is_dir ? '' : explorerIconHtml(e.name, false, false);
|
|
3905
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>'+
|
|
3906
4074
|
(e.is_dir ? '' : esc(String(e.size)))+'</td><td>'+esc(fmtMtime(e.mtime))+'</td>';
|
|
3907
|
-
|
|
3908
|
-
|
|
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');
|
|
3909
4078
|
tb.appendChild(tr);
|
|
3910
4079
|
}
|
|
3911
4080
|
}
|
|
@@ -3914,12 +4083,11 @@ function applyDetailRowSelectionHighlights(){
|
|
|
3914
4083
|
const tb = $('detail-rows');
|
|
3915
4084
|
if(!tb) return;
|
|
3916
4085
|
const entries = detailListEntries();
|
|
3917
|
-
const inDetailScope = explorerFsPathsEqual(treeSelectionParent, curPath);
|
|
3918
4086
|
tb.querySelectorAll('tr.detail-list-row').forEach(function(row){
|
|
3919
4087
|
const i = parseInt(row.dataset.detailIdx, 10);
|
|
3920
4088
|
if(i < 0 || i >= entries.length) return;
|
|
3921
|
-
const
|
|
3922
|
-
row.classList.toggle('selected', !!(
|
|
4089
|
+
const rowKey = explorerSelectionStorageKey(entries[i], curPath);
|
|
4090
|
+
row.classList.toggle('selected', !!(rowKey && selectedExplorerKeys.has(rowKey)));
|
|
3923
4091
|
});
|
|
3924
4092
|
}
|
|
3925
4093
|
|
|
@@ -3940,14 +4108,15 @@ function applyTreeRowSelectionHighlights(){
|
|
|
3940
4108
|
const par = String(row.dataset.treeParent || '');
|
|
3941
4109
|
const nm = String(row.dataset.treeName || '');
|
|
3942
4110
|
const inCurFolder = explorerFsPathsEqual(abs, cp);
|
|
3943
|
-
const
|
|
4111
|
+
const rowKey = nm ? explorerSelectionStorageKey({ name: nm }, par) : '';
|
|
4112
|
+
const selectedHere = !!(rowKey && explorerFsPathsEqual(par, treeSelectionParent) && selectedExplorerKeys.has(rowKey));
|
|
3944
4113
|
row.classList.toggle('selected', inCurFolder || selectedHere);
|
|
3945
4114
|
return;
|
|
3946
4115
|
}
|
|
3947
4116
|
const idx = parseInt(row.dataset.idx, 10);
|
|
3948
4117
|
if(!Number.isFinite(idx) || idx < 0 || idx >= lastEntries.length) return;
|
|
3949
|
-
const
|
|
3950
|
-
row.classList.toggle('selected',
|
|
4118
|
+
const rowKey = explorerSelectionStorageKey(lastEntries[idx], curPath);
|
|
4119
|
+
row.classList.toggle('selected', selectedExplorerKeys.has(rowKey));
|
|
3951
4120
|
});
|
|
3952
4121
|
}
|
|
3953
4122
|
|
|
@@ -4099,9 +4268,9 @@ function renderRootPicker(roots){
|
|
|
4099
4268
|
|
|
4100
4269
|
function pickRoot(absPath){
|
|
4101
4270
|
abortPreview();
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4271
|
+
lastSelectedExplorerKey = null;
|
|
4272
|
+
selectedExplorerKeys.clear();
|
|
4273
|
+
selectionAnchor = null;
|
|
4105
4274
|
clearSearchForFolderNavigation();
|
|
4106
4275
|
const ph = $('preview-head');
|
|
4107
4276
|
if(ph) ph.textContent = 'Preview';
|
|
@@ -4123,9 +4292,9 @@ function pickRoot(absPath){
|
|
|
4123
4292
|
function navigateIntoFolder(entry, fsBase){
|
|
4124
4293
|
if(!entry || !entry.is_dir) return;
|
|
4125
4294
|
abortPreview();
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4295
|
+
lastSelectedExplorerKey = null;
|
|
4296
|
+
selectedExplorerKeys.clear();
|
|
4297
|
+
selectionAnchor = null;
|
|
4129
4298
|
clearSearchForFolderNavigation();
|
|
4130
4299
|
const base = String(fsBase != null && fsBase !== '' ? fsBase : curPath);
|
|
4131
4300
|
const np = joinPath(base, entry.name);
|
|
@@ -4149,9 +4318,9 @@ function navigateToFolderAbs(absPath){
|
|
|
4149
4318
|
if(!p) return;
|
|
4150
4319
|
abortPreview();
|
|
4151
4320
|
revokeScreenshotBlob();
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4321
|
+
lastSelectedExplorerKey = null;
|
|
4322
|
+
selectedExplorerKeys.clear();
|
|
4323
|
+
selectionAnchor = null;
|
|
4155
4324
|
clearSearchForFolderNavigation();
|
|
4156
4325
|
rootsPickerMode = false;
|
|
4157
4326
|
curPath = p;
|
|
@@ -4353,8 +4522,8 @@ async function doConnect(){
|
|
|
4353
4522
|
bulkDeleteQueue = [];
|
|
4354
4523
|
bulkHfQueue = [];
|
|
4355
4524
|
bulkHfOpts = null;
|
|
4356
|
-
|
|
4357
|
-
|
|
4525
|
+
selectedExplorerKeys.clear();
|
|
4526
|
+
selectionAnchor = null;
|
|
4358
4527
|
/** Clears "Wait for shell to finish" / "Deleting…" / etc. left from the previous socket. */
|
|
4359
4528
|
setStatus('');
|
|
4360
4529
|
/** New socket or reconnect: must not send fs_* until auth again — doConnect clears onclose so authed is not reset there. */
|
|
@@ -4475,7 +4644,7 @@ async function doConnect(){
|
|
|
4475
4644
|
activeListRid = null;
|
|
4476
4645
|
activeRootsRid = null;
|
|
4477
4646
|
authed=false; afterAuthDone=false; curPath=''; lastEntries=[]; lastRead=null;
|
|
4478
|
-
|
|
4647
|
+
lastSelectedExplorerKey=null;
|
|
4479
4648
|
wantDeleteRid=null;
|
|
4480
4649
|
clearDeleteWatchdog();
|
|
4481
4650
|
if(wantShellRid){
|
|
@@ -4554,6 +4723,7 @@ function send(o){
|
|
|
4554
4723
|
|
|
4555
4724
|
function onMsg(m){
|
|
4556
4725
|
const t = m.type;
|
|
4726
|
+
if(t==='fs_screenshot_sidecar_result') return;
|
|
4557
4727
|
if(t==='relay_webrtc_availability'){
|
|
4558
4728
|
relayWebrtcSignaling = m.webrtc_signaling === true;
|
|
4559
4729
|
relayRtcIceServers = Array.isArray(m.rtc_ice_servers) ? m.rtc_ice_servers : null;
|
|
@@ -4562,7 +4732,7 @@ function onMsg(m){
|
|
|
4562
4732
|
forgeRtcReconnectAttempts = 0;
|
|
4563
4733
|
teardownForgeRtcExplorer();
|
|
4564
4734
|
} else if(authed && ws && ws.readyState === 1 && !forgeRtcProbeStarted){
|
|
4565
|
-
setTimeout(function(){ tryForgeRtcExplorerProbe(); },
|
|
4735
|
+
setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 120);
|
|
4566
4736
|
}
|
|
4567
4737
|
return;
|
|
4568
4738
|
}
|
|
@@ -4692,7 +4862,7 @@ function onMsg(m){
|
|
|
4692
4862
|
startViewerKeepalive();
|
|
4693
4863
|
forgeRtcReconnectAttempts = 0;
|
|
4694
4864
|
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
4695
|
-
setTimeout(function(){ tryForgeRtcExplorerProbe(); },
|
|
4865
|
+
setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 120);
|
|
4696
4866
|
sendFsRoots();
|
|
4697
4867
|
updateAgentShellHints();
|
|
4698
4868
|
} else afterAuth();
|
|
@@ -4821,9 +4991,9 @@ function onMsg(m){
|
|
|
4821
4991
|
}
|
|
4822
4992
|
|
|
4823
4993
|
const newPath = responsePath || curPath;
|
|
4824
|
-
if(!explorerFsPathsEqual(newPath, curPath) &&
|
|
4825
|
-
|
|
4826
|
-
|
|
4994
|
+
if(!explorerFsPathsEqual(newPath, curPath) && selectedExplorerKeys.size > 0){
|
|
4995
|
+
selectedExplorerKeys.clear();
|
|
4996
|
+
selectionAnchor = null;
|
|
4827
4997
|
}
|
|
4828
4998
|
curPath = newPath;
|
|
4829
4999
|
treeSelectionParent = curPath;
|
|
@@ -4845,8 +5015,9 @@ function onMsg(m){
|
|
|
4845
5015
|
const iconPart = e.is_dir ? '' : explorerIconHtml(e.name, false, false);
|
|
4846
5016
|
tr.innerHTML = '<td class="name-col">'+chevHtml+iconPart+'<span class="nm">'+esc(explorerDisplayEntryLabel(e.name))+'</span></td><td>'+typ+'</td><td>'+
|
|
4847
5017
|
(e.is_dir?'':esc(String(e.size)))+'</td><td>'+esc(fmtMtime(e.mtime))+'</td>';
|
|
4848
|
-
|
|
4849
|
-
|
|
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');
|
|
4850
5021
|
tb.appendChild(tr);
|
|
4851
5022
|
});
|
|
4852
5023
|
renderDetailPane();
|
|
@@ -5219,9 +5390,9 @@ function onMsg(m){
|
|
|
5219
5390
|
if(ph) ph.textContent = 'Preview';
|
|
5220
5391
|
$('preview').textContent = '(Deleted)';
|
|
5221
5392
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5393
|
+
lastSelectedExplorerKey = null;
|
|
5394
|
+
selectedExplorerKeys.clear();
|
|
5395
|
+
selectionAnchor = null;
|
|
5225
5396
|
setStatus('Deleted');
|
|
5226
5397
|
clearDeleteConfirmIntent();
|
|
5227
5398
|
refresh();
|
|
@@ -5663,7 +5834,7 @@ function afterAuth(){
|
|
|
5663
5834
|
startViewerKeepalive();
|
|
5664
5835
|
forgeRtcReconnectAttempts = 0;
|
|
5665
5836
|
if(forgeRtcReconnectTimer){ clearTimeout(forgeRtcReconnectTimer); forgeRtcReconnectTimer = null; }
|
|
5666
|
-
setTimeout(function(){ tryForgeRtcExplorerProbe(); },
|
|
5837
|
+
setTimeout(function(){ tryForgeRtcExplorerProbe(); }, 120);
|
|
5667
5838
|
sendFsRoots();
|
|
5668
5839
|
try { send({ type: 'get_info' }); } catch(e){}
|
|
5669
5840
|
syncOverlayXferHint();
|
|
@@ -5710,12 +5881,22 @@ function goUp(){
|
|
|
5710
5881
|
if(rootsPickerMode && !String(curPath || '').trim()) return;
|
|
5711
5882
|
send({type:'fs_parent', path: p, request_id: ridn()});
|
|
5712
5883
|
}
|
|
5884
|
+
/** Ordered selection for toolbar actions. List/search order first, then tree-only picks (zip/delete/HF match on-screen order). */
|
|
5713
5885
|
function selectedEntriesOrdered(){
|
|
5714
5886
|
const out = [];
|
|
5715
|
-
const
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
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);
|
|
5719
5900
|
return out;
|
|
5720
5901
|
}
|
|
5721
5902
|
function selectedRow(){
|
|
@@ -5733,6 +5914,15 @@ function selEntry(){
|
|
|
5733
5914
|
for(let k=0;k<pool.length;k++){
|
|
5734
5915
|
if(pool[k].name === nm) return pool[k];
|
|
5735
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
|
+
}
|
|
5736
5926
|
return null;
|
|
5737
5927
|
}
|
|
5738
5928
|
if(tr.classList.contains('detail-list-row')){
|
|
@@ -5766,7 +5956,7 @@ function viewSel(){
|
|
|
5766
5956
|
}
|
|
5767
5957
|
function sendOneHfFromEntry(e){
|
|
5768
5958
|
if(!bulkHfOpts) return;
|
|
5769
|
-
const fullPath =
|
|
5959
|
+
const fullPath = explorerEntryAbsPathResolved(e);
|
|
5770
5960
|
const r = ridn();
|
|
5771
5961
|
wantHfRid = r;
|
|
5772
5962
|
persistHfRid(r);
|
|
@@ -5839,7 +6029,11 @@ async function uploadHfSel(){
|
|
|
5839
6029
|
const createRepo = crEl ? !!crEl.checked : false;
|
|
5840
6030
|
const modeEl = $('hf-folder-mode');
|
|
5841
6031
|
const folderMode = modeEl && modeEl.value === 'tree' ? 'tree' : 'zip';
|
|
5842
|
-
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
|
+
}
|
|
5843
6037
|
const rid = ridn();
|
|
5844
6038
|
wantHfRid = rid;
|
|
5845
6039
|
persistHfRid(rid);
|
|
@@ -5849,7 +6043,7 @@ async function uploadHfSel(){
|
|
|
5849
6043
|
: null;
|
|
5850
6044
|
setXferStatus(
|
|
5851
6045
|
'HF upload starting… ' +
|
|
5852
|
-
(
|
|
6046
|
+
(fullPaths.length > 1 ? (fullPaths.length + ' selected entries (single zip commit)') : esc(entries[0].name))
|
|
5853
6047
|
);
|
|
5854
6048
|
if(useSessionRepo){
|
|
5855
6049
|
if(!sessionTable){
|
|
@@ -6191,14 +6385,39 @@ function fsZipXferPayload(offset){
|
|
|
6191
6385
|
return Object.assign({path: wantFolderZipPath}, base, xfer);
|
|
6192
6386
|
}
|
|
6193
6387
|
|
|
6194
|
-
function beginDownloadEntry(e, pickedWritable){
|
|
6388
|
+
function beginDownloadEntry(e, pickedWritable, opts){
|
|
6389
|
+
const o = opts || {};
|
|
6195
6390
|
snapshotXferStaging();
|
|
6196
6391
|
writeChain = Promise.resolve();
|
|
6197
6392
|
saveFileWritable = pickedWritable;
|
|
6198
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
|
+
}
|
|
6199
6418
|
if(e.is_dir){
|
|
6200
6419
|
wantFolderZipPaths = null;
|
|
6201
|
-
wantFolderZipPath =
|
|
6420
|
+
wantFolderZipPath = explorerEntryAbsPathResolved(e);
|
|
6202
6421
|
const r = ridn();
|
|
6203
6422
|
wantFolderZipRid = r;
|
|
6204
6423
|
wantFolderZipSaveName = '';
|
|
@@ -6208,7 +6427,7 @@ function beginDownloadEntry(e, pickedWritable){
|
|
|
6208
6427
|
setXferStatus('Zipping folder… '+esc(e.name)+(q ? ' ('+q+' queued)' : ''));
|
|
6209
6428
|
return;
|
|
6210
6429
|
}
|
|
6211
|
-
wantDownloadPath =
|
|
6430
|
+
wantDownloadPath = explorerEntryAbsPathResolved(e);
|
|
6212
6431
|
const r = ridn();
|
|
6213
6432
|
wantDownloadRid = r;
|
|
6214
6433
|
wantDownloadName = e.name;
|
|
@@ -6223,25 +6442,37 @@ async function downloadSel(){
|
|
|
6223
6442
|
if(!entries.length) return;
|
|
6224
6443
|
if(wantDownloadRid != null || wantFolderZipRid != null || wantHfRid != null){ setStatus('Download or HF upload in progress'); return; }
|
|
6225
6444
|
abortPreview();
|
|
6226
|
-
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;
|
|
6227
6451
|
let pickedWritable = null;
|
|
6228
|
-
if(canUseFileSystemPicker() && !preferSilentDownload()
|
|
6229
|
-
const e = entries[0];
|
|
6452
|
+
if(canUseFileSystemPicker() && !preferSilentDownload()){
|
|
6230
6453
|
try {
|
|
6231
|
-
if(
|
|
6454
|
+
if(multiZip){
|
|
6232
6455
|
pickedWritable = await (await window.showSaveFilePicker({
|
|
6233
|
-
suggestedName: safeDownloadName(
|
|
6456
|
+
suggestedName: safeDownloadName('forge-selection-'+uniq.length+'-items') + '.zip',
|
|
6234
6457
|
})).createWritable();
|
|
6235
6458
|
} else {
|
|
6236
|
-
|
|
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
|
+
}
|
|
6237
6468
|
}
|
|
6238
6469
|
} catch(err){
|
|
6239
6470
|
if(err && err.name === 'AbortError') return;
|
|
6240
6471
|
pickedWritable = null;
|
|
6241
6472
|
}
|
|
6242
6473
|
}
|
|
6243
|
-
if(
|
|
6244
|
-
wantFolderZipPaths =
|
|
6474
|
+
if(multiZip){
|
|
6475
|
+
wantFolderZipPaths = uniq;
|
|
6245
6476
|
wantFolderZipPath = wantFolderZipPaths[0] || '';
|
|
6246
6477
|
const r = ridn();
|
|
6247
6478
|
wantFolderZipRid = r;
|
|
@@ -6250,11 +6481,19 @@ async function downloadSel(){
|
|
|
6250
6481
|
wantFolderZipTotal = 0;
|
|
6251
6482
|
bulkDownloadQueue = [];
|
|
6252
6483
|
send(fsZipXferPayload(0));
|
|
6253
|
-
setXferStatus('Zipping '+
|
|
6484
|
+
setXferStatus('Zipping '+uniq.length+' item(s)…');
|
|
6254
6485
|
return;
|
|
6255
6486
|
}
|
|
6256
6487
|
bulkDownloadQueue = [];
|
|
6257
|
-
|
|
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
|
+
}
|
|
6258
6497
|
}
|
|
6259
6498
|
|
|
6260
6499
|
function deleteSel(){
|
|
@@ -6279,15 +6518,20 @@ function deleteSel(){
|
|
|
6279
6518
|
const pr = $('preview');
|
|
6280
6519
|
if(pr) pr.textContent = 'Stopping preview for delete…';
|
|
6281
6520
|
}
|
|
6282
|
-
const paths = entries.map(
|
|
6283
|
-
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){
|
|
6284
6527
|
const fullPath = paths[0];
|
|
6528
|
+
const disp = (entries[0] && entries[0].name) ? entries[0].name : fullPath;
|
|
6285
6529
|
if(deleteConfirmPath !== fullPath){
|
|
6286
6530
|
deleteConfirmPath = fullPath;
|
|
6287
6531
|
deleteConfirmBulkKey = '';
|
|
6288
6532
|
deleteConfirmForceIntent = xferForce();
|
|
6289
6533
|
deleteConfirmForceKillIntent = xferForceKill();
|
|
6290
|
-
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 + '"');
|
|
6291
6535
|
return;
|
|
6292
6536
|
}
|
|
6293
6537
|
deleteConfirmPath = '';
|
|
@@ -6309,7 +6553,7 @@ function deleteSel(){
|
|
|
6309
6553
|
deleteConfirmBulkKey = bulkKey;
|
|
6310
6554
|
deleteConfirmForceIntent = xferForce();
|
|
6311
6555
|
deleteConfirmForceKillIntent = xferForceKill();
|
|
6312
|
-
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');
|
|
6313
6557
|
return;
|
|
6314
6558
|
}
|
|
6315
6559
|
deleteConfirmBulkKey = '';
|
|
@@ -6359,22 +6603,22 @@ document.addEventListener('click', ev => {
|
|
|
6359
6603
|
treeSelectionParent = curPath;
|
|
6360
6604
|
const i = parseInt(trD.dataset.detailIdx, 10);
|
|
6361
6605
|
if(i < 0 || i >= lastEntries.length) return;
|
|
6362
|
-
const
|
|
6363
|
-
if(isShift &&
|
|
6364
|
-
const a = Math.min(
|
|
6365
|
-
const b = Math.max(
|
|
6366
|
-
if(!isMeta)
|
|
6367
|
-
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));
|
|
6368
6612
|
} else if(isMeta){
|
|
6369
|
-
if(
|
|
6370
|
-
else
|
|
6371
|
-
|
|
6613
|
+
if(selectedExplorerKeys.has(key)) selectedExplorerKeys.delete(key);
|
|
6614
|
+
else selectedExplorerKeys.add(key);
|
|
6615
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
6372
6616
|
} else {
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6617
|
+
selectedExplorerKeys.clear();
|
|
6618
|
+
selectedExplorerKeys.add(key);
|
|
6619
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
6376
6620
|
}
|
|
6377
|
-
|
|
6621
|
+
lastSelectedExplorerKey = key;
|
|
6378
6622
|
applyAllRowSelectionHighlights();
|
|
6379
6623
|
maybeResetDeleteConfirm();
|
|
6380
6624
|
return;
|
|
@@ -6396,9 +6640,9 @@ document.addEventListener('click', ev => {
|
|
|
6396
6640
|
cancelForgeTreeFoldClickTimer();
|
|
6397
6641
|
document.querySelectorAll('#detail-rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
6398
6642
|
if(!isMeta && !isShift){
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6643
|
+
selectedExplorerKeys.clear();
|
|
6644
|
+
selectionAnchor = null;
|
|
6645
|
+
lastSelectedExplorerKey = null;
|
|
6402
6646
|
}
|
|
6403
6647
|
document.querySelectorAll('#rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
6404
6648
|
tr.classList.add('selected');
|
|
@@ -6438,22 +6682,22 @@ document.addEventListener('click', ev => {
|
|
|
6438
6682
|
const pool = treeEntriesForParent(treeSelectionParent);
|
|
6439
6683
|
const ti = pool.findIndex(function(ent){ return ent.name === tr.dataset.treeName; });
|
|
6440
6684
|
if(ti < 0) return;
|
|
6441
|
-
const
|
|
6442
|
-
if(isShift &&
|
|
6443
|
-
const a = Math.min(
|
|
6444
|
-
const b = Math.max(
|
|
6445
|
-
if(!isMeta)
|
|
6446
|
-
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));
|
|
6447
6691
|
} else if(isMeta){
|
|
6448
|
-
if(
|
|
6449
|
-
else
|
|
6450
|
-
|
|
6692
|
+
if(selectedExplorerKeys.has(tkey)) selectedExplorerKeys.delete(tkey);
|
|
6693
|
+
else selectedExplorerKeys.add(tkey);
|
|
6694
|
+
selectionAnchor = { kind: 'tree', parent: String(treeSelectionParent || ''), idx: ti };
|
|
6451
6695
|
} else {
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6696
|
+
selectedExplorerKeys.clear();
|
|
6697
|
+
selectedExplorerKeys.add(tkey);
|
|
6698
|
+
selectionAnchor = { kind: 'tree', parent: String(treeSelectionParent || ''), idx: ti };
|
|
6455
6699
|
}
|
|
6456
|
-
|
|
6700
|
+
lastSelectedExplorerKey = tkey;
|
|
6457
6701
|
applyAllRowSelectionHighlights();
|
|
6458
6702
|
maybeResetDeleteConfirm();
|
|
6459
6703
|
return;
|
|
@@ -6464,28 +6708,28 @@ document.addEventListener('click', ev => {
|
|
|
6464
6708
|
document.querySelectorAll('#detail-rows tr').forEach(function(x){ x.classList.remove('selected'); });
|
|
6465
6709
|
const i = parseInt(tr.dataset.idx,10);
|
|
6466
6710
|
if(i < 0 || i >= lastEntries.length) return;
|
|
6467
|
-
const
|
|
6468
|
-
if(isShift &&
|
|
6469
|
-
const a = Math.min(
|
|
6470
|
-
const b = Math.max(
|
|
6471
|
-
if(!isMeta)
|
|
6472
|
-
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));
|
|
6473
6717
|
} else if(isMeta){
|
|
6474
|
-
if(
|
|
6475
|
-
else
|
|
6476
|
-
|
|
6718
|
+
if(selectedExplorerKeys.has(ikey)) selectedExplorerKeys.delete(ikey);
|
|
6719
|
+
else selectedExplorerKeys.add(ikey);
|
|
6720
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
6477
6721
|
} else {
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6722
|
+
selectedExplorerKeys.clear();
|
|
6723
|
+
selectedExplorerKeys.add(ikey);
|
|
6724
|
+
selectionAnchor = { kind: 'list', idx: i };
|
|
6481
6725
|
}
|
|
6482
|
-
|
|
6726
|
+
lastSelectedExplorerKey = ikey;
|
|
6483
6727
|
document.querySelectorAll('#rows tr').forEach(function(row){
|
|
6484
6728
|
if(row.classList.contains('root-row') || row.classList.contains('explorer-tree-computer')) return;
|
|
6485
6729
|
if(row.classList.contains('tree-entry')) return;
|
|
6486
6730
|
const idx = parseInt(row.dataset.idx, 10);
|
|
6487
6731
|
if(idx >= 0 && idx < lastEntries.length){
|
|
6488
|
-
row.classList.toggle('selected',
|
|
6732
|
+
row.classList.toggle('selected', selectedExplorerKeys.has(explorerSelectionStorageKey(lastEntries[idx], curPath)));
|
|
6489
6733
|
}
|
|
6490
6734
|
});
|
|
6491
6735
|
applyDetailRowSelectionHighlights();
|
|
@@ -6596,10 +6840,145 @@ function doDisconnect(){
|
|
|
6596
6840
|
* and shows status/errors at the top of the page — no dialog needed.
|
|
6597
6841
|
*/
|
|
6598
6842
|
// ─── Browser → Agent file upload ─────────────────────────────────────────────
|
|
6599
|
-
/** Upload progress tracker for browser→agent pushes */
|
|
6843
|
+
/** Upload progress tracker for browser→agent pushes (`kind`: one file or zip batch). */
|
|
6600
6844
|
let uploadPushQueue = [];
|
|
6601
6845
|
let uploadPushActive = false;
|
|
6602
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
|
+
|
|
6603
6982
|
function openUploadFilePicker(){
|
|
6604
6983
|
if(!authed || !ws || ws.readyState !== 1){
|
|
6605
6984
|
setStatus('Not connected');
|
|
@@ -6608,88 +6987,59 @@ function openUploadFilePicker(){
|
|
|
6608
6987
|
const el = $('upload-file-input');
|
|
6609
6988
|
if(el){ el.value = ''; el.click(); }
|
|
6610
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
|
+
}
|
|
6611
6998
|
|
|
6612
6999
|
document.addEventListener('change', function(ev){
|
|
6613
|
-
|
|
6614
|
-
|
|
7000
|
+
const t = ev.target;
|
|
7001
|
+
if(!t || (t.id !== 'upload-file-input' && t.id !== 'upload-folder-input')) return;
|
|
7002
|
+
const files = t.files;
|
|
6615
7003
|
if(!files || !files.length) return;
|
|
6616
7004
|
if(!authed || !ws || ws.readyState !== 1){
|
|
6617
7005
|
setStatus('Not connected — cannot upload');
|
|
6618
7006
|
return;
|
|
6619
7007
|
}
|
|
6620
7008
|
const arr = Array.from(files);
|
|
6621
|
-
|
|
7009
|
+
if(feFileListWantsZipBatch(arr)){
|
|
7010
|
+
uploadPushQueue.push({ kind: 'zip', files: arr });
|
|
7011
|
+
} else {
|
|
7012
|
+
uploadPushQueue.push({ kind: 'one', file: arr[0] });
|
|
7013
|
+
}
|
|
6622
7014
|
if(!uploadPushActive) drainUploadPushQueue();
|
|
6623
|
-
|
|
7015
|
+
t.value = '';
|
|
6624
7016
|
});
|
|
6625
7017
|
|
|
6626
7018
|
async function drainUploadPushQueue(){
|
|
6627
7019
|
uploadPushActive = true;
|
|
6628
7020
|
while(uploadPushQueue.length > 0){
|
|
6629
|
-
const
|
|
6630
|
-
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);
|
|
6631
7024
|
}
|
|
6632
7025
|
uploadPushActive = false;
|
|
6633
7026
|
}
|
|
6634
7027
|
|
|
6635
7028
|
async function sendFilePushOne(file){
|
|
6636
7029
|
const MAX = 20 * 1024 * 1024;
|
|
6637
|
-
if(file.size === 0 || file.size > MAX){
|
|
6638
|
-
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');
|
|
6639
7032
|
return;
|
|
6640
7033
|
}
|
|
6641
7034
|
setXferStatus('Uploading "' + esc(file.name) + '" to agent…');
|
|
6642
|
-
|
|
6643
|
-
if(btn) btn.disabled = true;
|
|
7035
|
+
setUploadButtonsDisabled(true);
|
|
6644
7036
|
try {
|
|
6645
7037
|
const ab = await file.arrayBuffer();
|
|
6646
|
-
|
|
6647
|
-
let b64 = '';
|
|
6648
|
-
const B64_CHUNK = 8192;
|
|
6649
|
-
for(let _i = 0; _i < bytes.length; _i += B64_CHUNK){
|
|
6650
|
-
b64 += btoa(String.fromCharCode.apply(null, bytes.subarray(_i, _i + B64_CHUNK)));
|
|
6651
|
-
}
|
|
6652
|
-
const targetPath = curPath || '';
|
|
6653
|
-
const rid = ridn();
|
|
6654
|
-
const wsRef = ws;
|
|
6655
|
-
if(!wsRef || wsRef.readyState !== 1) throw new Error('not connected');
|
|
6656
|
-
await new Promise(function(resolve, reject){
|
|
6657
|
-
const cleanup = function(){
|
|
6658
|
-
wsRef.removeEventListener('message', handler);
|
|
6659
|
-
wsRef.removeEventListener('close', closeHandler);
|
|
6660
|
-
clearTimeout(timer);
|
|
6661
|
-
};
|
|
6662
|
-
const timer = setTimeout(function(){
|
|
6663
|
-
cleanup();
|
|
6664
|
-
reject(new Error('timeout waiting for rc_file_push_result'));
|
|
6665
|
-
}, 30000);
|
|
6666
|
-
const handler = function(ev2){
|
|
6667
|
-
let msg;
|
|
6668
|
-
try { msg = JSON.parse(ev2.data); } catch{ return; }
|
|
6669
|
-
if(!msg || msg.request_id !== rid) return;
|
|
6670
|
-
cleanup();
|
|
6671
|
-
if(msg.type === 'rc_file_push_result'){
|
|
6672
|
-
if(msg.ok) resolve(msg);
|
|
6673
|
-
else reject(new Error(String(msg.error || 'push failed')));
|
|
6674
|
-
} else if(msg.type === 'fs_error'){
|
|
6675
|
-
reject(new Error(String(msg.error || 'fs_error')));
|
|
6676
|
-
}
|
|
6677
|
-
};
|
|
6678
|
-
const closeHandler = function(){
|
|
6679
|
-
cleanup();
|
|
6680
|
-
reject(new Error('WebSocket disconnected during upload'));
|
|
6681
|
-
};
|
|
6682
|
-
wsRef.addEventListener('message', handler);
|
|
6683
|
-
wsRef.addEventListener('close', closeHandler);
|
|
6684
|
-
send({ type: 'rc_file_push', name: file.name, b64: b64, path: targetPath, request_id: rid });
|
|
6685
|
-
});
|
|
6686
|
-
setXferStatus('Uploaded "' + esc(file.name) + '" → ' + esc(targetPath));
|
|
6687
|
-
refresh();
|
|
7038
|
+
await sendRcFilePushBytes(file.name, new Uint8Array(ab), 30000);
|
|
6688
7039
|
} catch(e){
|
|
6689
7040
|
setXferStatus('Upload failed "' + esc(file.name) + '": ' + esc(String(e && e.message ? e.message : e)));
|
|
6690
7041
|
} finally {
|
|
6691
|
-
|
|
6692
|
-
if(btn2) btn2.disabled = false;
|
|
7042
|
+
setUploadButtonsDisabled(false);
|
|
6693
7043
|
}
|
|
6694
7044
|
}
|
|
6695
7045
|
|