openclacky 1.5.2 → 1.5.4
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.
- checksums.yaml +4 -4
- data/.dockerignore +13 -0
- data/CHANGELOG.md +50 -0
- data/Dockerfile +21 -1
- data/README.md +26 -7
- data/README_CN.md +26 -7
- data/README_JA.md +26 -7
- data/lib/clacky/agent/message_compressor.rb +4 -29
- data/lib/clacky/agent/message_compressor_helper.rb +13 -15
- data/lib/clacky/agent/session_serializer.rb +6 -0
- data/lib/clacky/agent.rb +13 -1
- data/lib/clacky/agent_config.rb +42 -11
- data/lib/clacky/brand_config.rb +32 -12
- data/lib/clacky/client.rb +7 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +15 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +2 -6
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +11 -4
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +5 -1
- data/lib/clacky/default_parsers/xlsx_parser.py +66 -0
- data/lib/clacky/extension/api_extension.rb +3 -2
- data/lib/clacky/message_format/anthropic.rb +27 -1
- data/lib/clacky/message_format/open_ai.rb +92 -4
- data/lib/clacky/providers.rb +67 -14
- data/lib/clacky/server/http_server.rb +361 -36
- data/lib/clacky/server/project_manager.rb +150 -0
- data/lib/clacky/server/server_master.rb +7 -0
- data/lib/clacky/server/session_registry.rb +14 -1
- data/lib/clacky/session_manager.rb +5 -4
- data/lib/clacky/tools/file_reader.rb +2 -1
- data/lib/clacky/utils/model_pricing.rb +77 -18
- data/lib/clacky/utils/parser_manager.rb +115 -7
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1053 -143
- data/lib/clacky/web/app.js +19 -5
- data/lib/clacky/web/components/code-editor.js +50 -12
- data/lib/clacky/web/components/notify.js +139 -22
- data/lib/clacky/web/core/aside.js +21 -0
- data/lib/clacky/web/features/billing/view.js +2 -1
- data/lib/clacky/web/features/brand/store.js +3 -1
- data/lib/clacky/web/features/brand/view.js +29 -5
- data/lib/clacky/web/features/extensions/store.js +15 -6
- data/lib/clacky/web/features/extensions/view.js +8 -4
- data/lib/clacky/web/features/new-session/view.js +2 -2
- data/lib/clacky/web/features/trash/store.js +23 -9
- data/lib/clacky/web/features/trash/view.js +51 -0
- data/lib/clacky/web/features/workspace/store.js +9 -0
- data/lib/clacky/web/features/workspace/view.js +8 -1
- data/lib/clacky/web/i18n.js +99 -4
- data/lib/clacky/web/index.html +70 -17
- data/lib/clacky/web/projects.js +1143 -0
- data/lib/clacky/web/sessions.js +318 -537
- data/lib/clacky/web/settings.js +146 -7
- data/lib/clacky/web/ws-dispatcher.js +59 -1
- data/scripts/build/src/install_system_deps.sh.cc +11 -3
- data/scripts/install_system_deps.sh +11 -3
- metadata +5 -2
- data/lib/clacky/default_parsers/xlsx_parser.rb +0 -121
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -59,6 +59,10 @@ const Sessions = (() => {
|
|
|
59
59
|
// Flushed in _fetchHistory after the fragment is appended to the DOM.
|
|
60
60
|
let _pendingStdoutLines = null; // string[] | null
|
|
61
61
|
|
|
62
|
+
// ── Chat section collapse ──────────────────────────────────────────────────
|
|
63
|
+
const _CHAT_COLLAPSE_KEY = "clacky_chat_section_collapsed";
|
|
64
|
+
let _chatCollapsed = localStorage.getItem(_CHAT_COLLAPSE_KEY) === "1";
|
|
65
|
+
|
|
62
66
|
// ── Markdown renderer ──────────────────────────────────────────────────
|
|
63
67
|
//
|
|
64
68
|
// Renders assistant message text as Markdown HTML using the marked library.
|
|
@@ -433,6 +437,25 @@ const Sessions = (() => {
|
|
|
433
437
|
}
|
|
434
438
|
}
|
|
435
439
|
|
|
440
|
+
function _initSectionCollapse() {
|
|
441
|
+
const chatHeader = document.getElementById("chat-section-header");
|
|
442
|
+
const sessionList = document.getElementById("session-list");
|
|
443
|
+
if (!chatHeader || !sessionList) return;
|
|
444
|
+
|
|
445
|
+
if (_chatCollapsed) {
|
|
446
|
+
sessionList.classList.add("chat-section-collapsed");
|
|
447
|
+
chatHeader.classList.add("chat-header-collapsed");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
chatHeader.addEventListener("click", e => {
|
|
451
|
+
if (e.target.closest(".sidebar-divider-actions")) return;
|
|
452
|
+
_chatCollapsed = !_chatCollapsed;
|
|
453
|
+
localStorage.setItem(_CHAT_COLLAPSE_KEY, _chatCollapsed ? "1" : "0");
|
|
454
|
+
sessionList.classList.toggle("chat-section-collapsed", _chatCollapsed);
|
|
455
|
+
chatHeader.classList.toggle("chat-header-collapsed", _chatCollapsed);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
436
459
|
function _initEmptyHint() {
|
|
437
460
|
const messages = RenderTarget.outer();
|
|
438
461
|
if (!messages) return;
|
|
@@ -551,7 +574,7 @@ const Sessions = (() => {
|
|
|
551
574
|
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5 MB — hard reject before compression
|
|
552
575
|
const MAX_IMAGE_BYTES_SEND = 512 * 1024; // 512 KB — target after compression
|
|
553
576
|
const MAX_IMAGE_LONG_EDGE = 1920; // px — scale down if larger
|
|
554
|
-
const MAX_FILE_BYTES =
|
|
577
|
+
const MAX_FILE_BYTES = 300 * 1024 * 1024; // 300 MB
|
|
555
578
|
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
556
579
|
|
|
557
580
|
function _isAcceptedImage(file) {
|
|
@@ -662,7 +685,7 @@ const Sessions = (() => {
|
|
|
662
685
|
|
|
663
686
|
function _addGenericFile(file) {
|
|
664
687
|
if (file.size > MAX_FILE_BYTES) {
|
|
665
|
-
alert(I18n.t("chat.file.tooLarge", { name: file.name, max: "
|
|
688
|
+
alert(I18n.t("chat.file.tooLarge", { name: file.name, max: "300 MB" }));
|
|
666
689
|
return;
|
|
667
690
|
}
|
|
668
691
|
// Upload file to server via HTTP — only the path is returned, no base64 in memory
|
|
@@ -2314,6 +2337,15 @@ const Sessions = (() => {
|
|
|
2314
2337
|
find: id => _sessions.find(s => s.id === id)
|
|
2315
2338
|
|| _extraSessions.find(s => s.id === id),
|
|
2316
2339
|
|
|
2340
|
+
/** Return the next available "Session N" name based on loaded sessions. */
|
|
2341
|
+
nextDefaultName() {
|
|
2342
|
+
const maxN = _sessions.reduce((max, s) => {
|
|
2343
|
+
const m = s.name && s.name.match(/^Session (\d+)$/);
|
|
2344
|
+
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
2345
|
+
}, 0);
|
|
2346
|
+
return "Session " + (maxN + 1);
|
|
2347
|
+
},
|
|
2348
|
+
|
|
2317
2349
|
// Async variant of `find`: when not found in memory, falls back to
|
|
2318
2350
|
// GET /api/sessions/:id which returns the on-disk session merged with
|
|
2319
2351
|
// any live in-memory state (see SessionRegistry#snapshot in
|
|
@@ -2355,6 +2387,9 @@ const Sessions = (() => {
|
|
|
2355
2387
|
init() {
|
|
2356
2388
|
_initNewMessageBanner();
|
|
2357
2389
|
_initEmptyHint();
|
|
2390
|
+
|
|
2391
|
+
_initSectionCollapse();
|
|
2392
|
+
|
|
2358
2393
|
_initNewSessionControls();
|
|
2359
2394
|
_initComposer();
|
|
2360
2395
|
_initSearch();
|
|
@@ -2438,11 +2473,7 @@ const Sessions = (() => {
|
|
|
2438
2473
|
// Returns the created session.
|
|
2439
2474
|
async startWith(command, { name, source = "setup", display = null } = {}) {
|
|
2440
2475
|
if (!name) {
|
|
2441
|
-
|
|
2442
|
-
const m = s.name && s.name.match(/^Session (\d+)$/);
|
|
2443
|
-
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
2444
|
-
}, 0);
|
|
2445
|
-
name = "Session " + (maxN + 1);
|
|
2476
|
+
name = Sessions.nextDefaultName();
|
|
2446
2477
|
}
|
|
2447
2478
|
|
|
2448
2479
|
const res = await fetch("/api/sessions", {
|
|
@@ -2494,6 +2525,28 @@ const Sessions = (() => {
|
|
|
2494
2525
|
_drafts.delete(id);
|
|
2495
2526
|
},
|
|
2496
2527
|
|
|
2528
|
+
/** Clear project_id from all sessions that belong to a deleted project. */
|
|
2529
|
+
clearProjectId(projectId) {
|
|
2530
|
+
_sessions.forEach(s => {
|
|
2531
|
+
if (s.project_id === projectId) s.project_id = null;
|
|
2532
|
+
});
|
|
2533
|
+
},
|
|
2534
|
+
|
|
2535
|
+
/** Remove all sessions belonging to a deleted project from local memory. */
|
|
2536
|
+
removeByProjectId(projectId) {
|
|
2537
|
+
for (let i = _sessions.length - 1; i >= 0; i--) {
|
|
2538
|
+
if (_sessions[i].project_id === projectId) _sessions.splice(i, 1);
|
|
2539
|
+
}
|
|
2540
|
+
},
|
|
2541
|
+
|
|
2542
|
+
/**
|
|
2543
|
+
* Public wrapper so Projects.renderSection() can reuse the same item
|
|
2544
|
+
* renderer without duplicating session-item DOM/event logic.
|
|
2545
|
+
*/
|
|
2546
|
+
renderSessionItem(container, s) {
|
|
2547
|
+
_renderSessionItem(container, s);
|
|
2548
|
+
},
|
|
2549
|
+
|
|
2497
2550
|
/** Load the next page of older sessions (unified time cursor). */
|
|
2498
2551
|
async loadMore() {
|
|
2499
2552
|
if (_loadingMore || !_hasMore) return;
|
|
@@ -2517,7 +2570,7 @@ const Sessions = (() => {
|
|
|
2517
2570
|
return (!min || t < min) ? t : min;
|
|
2518
2571
|
}, null);
|
|
2519
2572
|
|
|
2520
|
-
const params = new URLSearchParams({ limit: "
|
|
2573
|
+
const params = new URLSearchParams({ limit: "15", exclude_type: "cron" });
|
|
2521
2574
|
if (oldest) params.set("before", oldest);
|
|
2522
2575
|
if (_filter.q) params.set("q", _filter.q);
|
|
2523
2576
|
if (_filter.date) params.set("date", _filter.date);
|
|
@@ -2560,7 +2613,7 @@ const Sessions = (() => {
|
|
|
2560
2613
|
Sessions.renderList();
|
|
2561
2614
|
|
|
2562
2615
|
try {
|
|
2563
|
-
const params = new URLSearchParams({ limit: "
|
|
2616
|
+
const params = new URLSearchParams({ limit: "15", type: "cron" });
|
|
2564
2617
|
if (_cronBefore) params.set("before", _cronBefore);
|
|
2565
2618
|
|
|
2566
2619
|
const res = await fetch(`/api/sessions?${params}`);
|
|
@@ -2798,6 +2851,7 @@ const Sessions = (() => {
|
|
|
2798
2851
|
Sessions.remove(id);
|
|
2799
2852
|
if (id === Sessions.activeId) Router.navigate("welcome");
|
|
2800
2853
|
Sessions.renderList();
|
|
2854
|
+
Projects.renderSection();
|
|
2801
2855
|
} else {
|
|
2802
2856
|
const data = await res.json().catch(() => ({}));
|
|
2803
2857
|
console.error("Failed to delete session:", data.error || res.status);
|
|
@@ -2806,6 +2860,7 @@ const Sessions = (() => {
|
|
|
2806
2860
|
Sessions.remove(id);
|
|
2807
2861
|
if (id === Sessions.activeId) Router.navigate("welcome");
|
|
2808
2862
|
Sessions.renderList();
|
|
2863
|
+
Projects.renderSection();
|
|
2809
2864
|
}
|
|
2810
2865
|
}
|
|
2811
2866
|
// Server also broadcasts session_deleted WS event; Sessions.remove() is idempotent
|
|
@@ -2906,7 +2961,10 @@ const Sessions = (() => {
|
|
|
2906
2961
|
|
|
2907
2962
|
// ── Sidebar list always shows the full session set, sorted ───────
|
|
2908
2963
|
// Search/filter no longer touches this list — it lives in the overlay.
|
|
2909
|
-
|
|
2964
|
+
// Sessions assigned to a project are shown in the project section below.
|
|
2965
|
+
const visible = [..._sessions]
|
|
2966
|
+
.filter(s => !s.project_id)
|
|
2967
|
+
.sort(byPinnedAndTime);
|
|
2910
2968
|
|
|
2911
2969
|
// ── Split cron vs non-cron for folding ───────────────────────────
|
|
2912
2970
|
const isCronView = _cronView;
|
|
@@ -2954,8 +3012,6 @@ const Sessions = (() => {
|
|
|
2954
3012
|
// Normal list view, no cron sessions
|
|
2955
3013
|
visible.forEach(s => _renderSessionItem(list, s));
|
|
2956
3014
|
}
|
|
2957
|
-
|
|
2958
|
-
// Empty state fallback
|
|
2959
3015
|
if (list.children.length === 0) {
|
|
2960
3016
|
list.innerHTML = `<div class="session-empty">${I18n.t("sessions.empty")}</div>`;
|
|
2961
3017
|
}
|
|
@@ -3009,6 +3065,7 @@ const Sessions = (() => {
|
|
|
3009
3065
|
if (res.ok) {
|
|
3010
3066
|
Sessions.patch(sessionId, { name: newName });
|
|
3011
3067
|
Sessions.renderList();
|
|
3068
|
+
Projects.renderSection();
|
|
3012
3069
|
} else {
|
|
3013
3070
|
console.error("Rename failed:", await res.text());
|
|
3014
3071
|
}
|
|
@@ -3076,7 +3133,13 @@ const Sessions = (() => {
|
|
|
3076
3133
|
const iconRename = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4z"/></svg>`;
|
|
3077
3134
|
const iconTrash = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`;
|
|
3078
3135
|
|
|
3136
|
+
// Pin option is only shown for sessions that are NOT in a project
|
|
3079
3137
|
const pinLabel = session.pinned ? I18n.t("sessions.actions.unpin") : I18n.t("sessions.actions.pin");
|
|
3138
|
+
const pinItemHtml = `
|
|
3139
|
+
<div class="session-actions-menu-item" data-action="pin">
|
|
3140
|
+
<span class="session-actions-menu-icon">${iconPin}</span>
|
|
3141
|
+
<span class="session-actions-menu-label">${escapeHtml(pinLabel)}</span>
|
|
3142
|
+
</div>`;
|
|
3080
3143
|
|
|
3081
3144
|
const menu = document.createElement("div");
|
|
3082
3145
|
menu.className = "session-actions-menu";
|
|
@@ -3085,10 +3148,7 @@ const Sessions = (() => {
|
|
|
3085
3148
|
<span class="session-actions-menu-icon">${iconFork}</span>
|
|
3086
3149
|
<span class="session-actions-menu-label">${escapeHtml(I18n.t("sessions.actions.fork"))}</span>
|
|
3087
3150
|
</div>
|
|
3088
|
-
|
|
3089
|
-
<span class="session-actions-menu-icon">${iconPin}</span>
|
|
3090
|
-
<span class="session-actions-menu-label">${escapeHtml(pinLabel)}</span>
|
|
3091
|
-
</div>
|
|
3151
|
+
${pinItemHtml}
|
|
3092
3152
|
<div class="session-actions-menu-item" data-action="rename">
|
|
3093
3153
|
<span class="session-actions-menu-icon">${iconRename}</span>
|
|
3094
3154
|
<span class="session-actions-menu-label">${escapeHtml(I18n.t("sessions.actions.rename"))}</span>
|
|
@@ -3167,6 +3227,7 @@ const Sessions = (() => {
|
|
|
3167
3227
|
// Update local state
|
|
3168
3228
|
session.pinned = newPinnedState;
|
|
3169
3229
|
Sessions.renderList();
|
|
3230
|
+
Projects.renderSection();
|
|
3170
3231
|
} else {
|
|
3171
3232
|
console.error("Toggle pin failed:", await res.text());
|
|
3172
3233
|
}
|
|
@@ -3190,6 +3251,7 @@ const Sessions = (() => {
|
|
|
3190
3251
|
if (res.ok) {
|
|
3191
3252
|
Sessions.remove(sessionId);
|
|
3192
3253
|
Sessions.renderList();
|
|
3254
|
+
Projects.renderSection();
|
|
3193
3255
|
// If deleted session was active, switch to welcome
|
|
3194
3256
|
if (sessionId === _activeId) {
|
|
3195
3257
|
Router.navigate("welcome");
|
|
@@ -4704,18 +4766,13 @@ const Sessions = (() => {
|
|
|
4704
4766
|
}); return dirs;
|
|
4705
4767
|
}
|
|
4706
4768
|
|
|
4707
|
-
// Build a
|
|
4708
|
-
function buildDirNode(entry,
|
|
4769
|
+
// Build a flat list row for a directory entry (Finder-style)
|
|
4770
|
+
function buildDirNode(entry, _depth) {
|
|
4709
4771
|
const node = document.createElement("div");
|
|
4710
4772
|
node.className = "dp-node";
|
|
4711
|
-
node.dataset.depth = depth; // Store depth for child expansion
|
|
4712
4773
|
|
|
4713
4774
|
const row = document.createElement("div");
|
|
4714
4775
|
row.className = "dp-row";
|
|
4715
|
-
row.style.paddingLeft = `${depth * 16 + 8}px`;
|
|
4716
|
-
const caret = document.createElement("span");
|
|
4717
|
-
caret.className = "dp-caret";
|
|
4718
|
-
caret.innerHTML = ICON_CARET_SVG;
|
|
4719
4776
|
|
|
4720
4777
|
const icon = document.createElement("span");
|
|
4721
4778
|
icon.className = "dp-icon";
|
|
@@ -4725,192 +4782,266 @@ const Sessions = (() => {
|
|
|
4725
4782
|
name.className = "dp-name";
|
|
4726
4783
|
name.textContent = entry.name;
|
|
4727
4784
|
|
|
4728
|
-
row.appendChild(caret);
|
|
4729
4785
|
row.appendChild(icon);
|
|
4730
4786
|
row.appendChild(name);
|
|
4731
4787
|
node.appendChild(row);
|
|
4732
4788
|
|
|
4733
|
-
|
|
4734
|
-
children.className = "dp-children";
|
|
4735
|
-
children.style.display = "none";
|
|
4736
|
-
node.appendChild(children);
|
|
4737
|
-
|
|
4738
|
-
// Single-click: select directory (show path) + expand/collapse
|
|
4789
|
+
// Single-click: select this directory
|
|
4739
4790
|
let clickTimer = null;
|
|
4740
4791
|
row.addEventListener("click", (e) => {
|
|
4741
4792
|
e.stopPropagation();
|
|
4742
4793
|
if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; }
|
|
4743
4794
|
clickTimer = setTimeout(() => {
|
|
4744
4795
|
clickTimer = null;
|
|
4745
|
-
|
|
4746
|
-
modal.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
4796
|
+
listContainer.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
4747
4797
|
row.classList.add("selected");
|
|
4748
4798
|
selectedPath = entry.absPath;
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
toggleExpand(entry, caret, children);
|
|
4752
|
-
}, 250);
|
|
4799
|
+
refreshBreadcrumb(entry.absPath);
|
|
4800
|
+
}, 200);
|
|
4753
4801
|
});
|
|
4754
4802
|
|
|
4755
4803
|
// Double-click: enter directory (navigate into it)
|
|
4756
4804
|
row.addEventListener("dblclick", (e) => {
|
|
4757
4805
|
e.stopPropagation();
|
|
4758
4806
|
if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; }
|
|
4759
|
-
|
|
4760
|
-
loadTreeForPath(entry.absPath, entry.absolute);
|
|
4807
|
+
navigateTo(entry.absPath, true);
|
|
4761
4808
|
});
|
|
4762
4809
|
|
|
4763
|
-
// Make the row addressable for inline-edit invoked elsewhere
|
|
4764
|
-
// (e.g. from the "+ New folder" button placing its placeholder).
|
|
4765
4810
|
node._dpEntry = entry;
|
|
4766
4811
|
node._dpName = name;
|
|
4767
4812
|
node._dpRow = row;
|
|
4768
4813
|
return node;
|
|
4769
4814
|
}
|
|
4770
4815
|
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4816
|
+
// Create modal overlay
|
|
4817
|
+
const overlay = document.createElement("div");
|
|
4818
|
+
overlay.className = "modal-overlay";
|
|
4819
|
+
|
|
4820
|
+
// Create modal content — Finder-style layout
|
|
4821
|
+
const modal = document.createElement("div");
|
|
4822
|
+
modal.className = "modal-content dp-modal";
|
|
4823
|
+
|
|
4824
|
+
// ── Toolbar ──────────────────────────────────────────────────────
|
|
4825
|
+
const toolbar = document.createElement("div");
|
|
4826
|
+
toolbar.className = "dp-toolbar";
|
|
4827
|
+
|
|
4828
|
+
// Back / Forward buttons
|
|
4829
|
+
const backBtn = document.createElement("button");
|
|
4830
|
+
backBtn.className = "dp-nav-btn";
|
|
4831
|
+
backBtn.title = t("sib.dir.back", "后退");
|
|
4832
|
+
backBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`;
|
|
4833
|
+
backBtn.disabled = true;
|
|
4834
|
+
|
|
4835
|
+
const fwdBtn = document.createElement("button");
|
|
4836
|
+
fwdBtn.className = "dp-nav-btn";
|
|
4837
|
+
fwdBtn.title = t("sib.dir.forward", "前进");
|
|
4838
|
+
fwdBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`;
|
|
4839
|
+
fwdBtn.disabled = true;
|
|
4840
|
+
|
|
4841
|
+
const navBtns = document.createElement("div");
|
|
4842
|
+
navBtns.className = "dp-nav-btns";
|
|
4843
|
+
navBtns.appendChild(backBtn);
|
|
4844
|
+
navBtns.appendChild(fwdBtn);
|
|
4845
|
+
|
|
4846
|
+
// Breadcrumb
|
|
4847
|
+
const breadcrumb = document.createElement("div");
|
|
4848
|
+
breadcrumb.className = "dp-breadcrumb";
|
|
4849
|
+
|
|
4850
|
+
toolbar.appendChild(navBtns);
|
|
4851
|
+
toolbar.appendChild(breadcrumb);
|
|
4852
|
+
modal.appendChild(toolbar);
|
|
4853
|
+
|
|
4854
|
+
// ── Column header ─────────────────────────────────────────────────
|
|
4855
|
+
const colHeader = document.createElement("div");
|
|
4856
|
+
colHeader.className = "dp-col-header";
|
|
4857
|
+
const colName = document.createElement("span");
|
|
4858
|
+
colName.className = "dp-col-name";
|
|
4859
|
+
colName.textContent = t("sib.dir.colName", "名称");
|
|
4860
|
+
// Hidden files toggle (right side of header)
|
|
4861
|
+
const hiddenToggle = document.createElement("label");
|
|
4862
|
+
hiddenToggle.className = "dp-hidden-toggle";
|
|
4863
|
+
const hiddenCheckbox = document.createElement("input");
|
|
4864
|
+
hiddenCheckbox.type = "checkbox";
|
|
4865
|
+
hiddenCheckbox.checked = false;
|
|
4866
|
+
const hiddenLabelText = document.createElement("span");
|
|
4867
|
+
hiddenLabelText.textContent = t("sib.dir.showHidden", "显示隐藏文件");
|
|
4868
|
+
hiddenToggle.appendChild(hiddenCheckbox);
|
|
4869
|
+
hiddenToggle.appendChild(hiddenLabelText);
|
|
4870
|
+
colHeader.appendChild(colName);
|
|
4871
|
+
colHeader.appendChild(hiddenToggle);
|
|
4872
|
+
modal.appendChild(colHeader);
|
|
4873
|
+
|
|
4874
|
+
// ── List container ────────────────────────────────────────────────
|
|
4875
|
+
const listContainer = document.createElement("div");
|
|
4876
|
+
listContainer.className = "dp-list";
|
|
4877
|
+
listContainer.innerHTML = `<div class="dp-loading">${t("sib.dir.loading", "加载中...")}</div>`;
|
|
4878
|
+
modal.appendChild(listContainer);
|
|
4879
|
+
|
|
4880
|
+
// ── Footer ────────────────────────────────────────────────────────
|
|
4881
|
+
const footer = document.createElement("div");
|
|
4882
|
+
footer.className = "dp-footer";
|
|
4883
|
+
|
|
4884
|
+
const newFolderBtn = document.createElement("button");
|
|
4885
|
+
newFolderBtn.className = "btn btn-secondary btn-sm dp-newfolder-btn";
|
|
4886
|
+
newFolderBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span>${t("sib.dir.newFolder", "新建文件夹")}</span>`;
|
|
4887
|
+
|
|
4888
|
+
const footerActions = document.createElement("div");
|
|
4889
|
+
footerActions.className = "dp-footer-actions";
|
|
4890
|
+
|
|
4891
|
+
const cancelButton = document.createElement("button");
|
|
4892
|
+
cancelButton.className = "btn btn-secondary";
|
|
4893
|
+
cancelButton.textContent = t("sib.dir.cancel", "取消");
|
|
4894
|
+
cancelButton.onclick = () => { overlay.remove(); resolve(null); };
|
|
4895
|
+
|
|
4896
|
+
const confirmButton = document.createElement("button");
|
|
4897
|
+
confirmButton.className = "btn btn-primary";
|
|
4898
|
+
confirmButton.textContent = t("sib.dir.confirm", "确认");
|
|
4899
|
+
confirmButton.onclick = () => { overlay.remove(); resolve(selectedPath || null); };
|
|
4900
|
+
|
|
4901
|
+
footerActions.appendChild(cancelButton);
|
|
4902
|
+
footerActions.appendChild(confirmButton);
|
|
4903
|
+
footer.appendChild(newFolderBtn);
|
|
4904
|
+
footer.appendChild(footerActions);
|
|
4905
|
+
modal.appendChild(footer);
|
|
4906
|
+
|
|
4907
|
+
overlay.appendChild(modal);
|
|
4908
|
+
document.body.appendChild(overlay);
|
|
4909
|
+
|
|
4910
|
+
// ── History stack for back/fwd ────────────────────────────────────
|
|
4911
|
+
const history = [];
|
|
4912
|
+
let historyIndex = -1;
|
|
4913
|
+
|
|
4914
|
+
function refreshNavBtns() {
|
|
4915
|
+
backBtn.disabled = historyIndex <= 0;
|
|
4916
|
+
fwdBtn.disabled = historyIndex >= history.length - 1;
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
// Build breadcrumb from an absolute path
|
|
4920
|
+
function refreshBreadcrumb(absPath) {
|
|
4921
|
+
breadcrumb.innerHTML = "";
|
|
4922
|
+
if (!absPath) return;
|
|
4923
|
+
const parts = absPath.replace(/\/+$/, "").split("/");
|
|
4924
|
+
// parts[0] is "" for absolute paths starting with /
|
|
4925
|
+
parts.forEach((part, i) => {
|
|
4926
|
+
if (i === 0 && part === "") {
|
|
4927
|
+
// Root "/"
|
|
4928
|
+
const seg = document.createElement("span");
|
|
4929
|
+
seg.className = "dp-bc-seg";
|
|
4930
|
+
seg.textContent = "/";
|
|
4931
|
+
seg.addEventListener("click", () => navigateTo("/", true));
|
|
4932
|
+
breadcrumb.appendChild(seg);
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
if (!part) return;
|
|
4936
|
+
const sep = document.createElement("span");
|
|
4937
|
+
sep.className = "dp-bc-sep";
|
|
4938
|
+
sep.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`;
|
|
4939
|
+
const seg = document.createElement("span");
|
|
4940
|
+
seg.className = "dp-bc-seg";
|
|
4941
|
+
seg.textContent = part;
|
|
4942
|
+
const targetPath = parts.slice(0, i + 1).join("/") || "/";
|
|
4943
|
+
seg.addEventListener("click", () => navigateTo(targetPath, true));
|
|
4944
|
+
// Last segment = current dir, styled differently
|
|
4945
|
+
if (i === parts.length - 1) {
|
|
4946
|
+
seg.classList.add("dp-bc-current");
|
|
4947
|
+
// Add folder icon before last segment
|
|
4948
|
+
const folderIcon = document.createElement("span");
|
|
4949
|
+
folderIcon.className = "dp-bc-folder-icon";
|
|
4950
|
+
folderIcon.innerHTML = ICON_FOLDER_SVG;
|
|
4951
|
+
breadcrumb.appendChild(sep);
|
|
4952
|
+
breadcrumb.appendChild(folderIcon);
|
|
4953
|
+
} else {
|
|
4954
|
+
breadcrumb.appendChild(sep);
|
|
4955
|
+
}
|
|
4956
|
+
breadcrumb.appendChild(seg);
|
|
4957
|
+
});
|
|
4958
|
+
// Always scroll to show the rightmost (current) segment
|
|
4959
|
+
requestAnimationFrame(() => { breadcrumb.scrollLeft = breadcrumb.scrollWidth; });
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4962
|
+
// Core navigation function
|
|
4963
|
+
async function navigateTo(dirPath, absolute = true, pushHistory = true) {
|
|
4964
|
+
listContainer.innerHTML = `<div class="dp-loading">${t("sib.dir.loading", "加载中...")}</div>`;
|
|
4965
|
+
listContainer.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
4966
|
+
|
|
4967
|
+
// Push to history
|
|
4968
|
+
if (pushHistory) {
|
|
4969
|
+
// Trim forward history
|
|
4970
|
+
history.splice(historyIndex + 1);
|
|
4971
|
+
history.push(dirPath);
|
|
4972
|
+
historyIndex = history.length - 1;
|
|
4973
|
+
refreshNavBtns();
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4976
|
+
selectedPath = dirPath;
|
|
4977
|
+
refreshBreadcrumb(dirPath);
|
|
4978
|
+
|
|
4979
|
+
const useAbsolute = absolute || sessionLess || dirPath.startsWith("/");
|
|
4980
|
+
let relPath = dirPath;
|
|
4981
|
+
if (!sessionLess && !useAbsolute && rootDir && (dirPath === rootDir || dirPath.startsWith(rootDir + "/"))) {
|
|
4982
|
+
relPath = dirPath.substring(rootDir.length).replace(/^\/+/, "");
|
|
4777
4983
|
}
|
|
4778
|
-
|
|
4779
|
-
children.style.display = "flex";
|
|
4780
|
-
if (children.dataset.loaded === "1") return;
|
|
4781
|
-
children.innerHTML = `<div class="dp-loading">${t("sib.dir.loading", "加载中...")}</div>`;
|
|
4984
|
+
|
|
4782
4985
|
try {
|
|
4783
|
-
const dirs = await fetchDirs(
|
|
4784
|
-
|
|
4986
|
+
const dirs = await fetchDirs(relPath, useAbsolute);
|
|
4987
|
+
// After first relative load, rootDir is now known — refresh breadcrumb + selectedPath
|
|
4988
|
+
if (rootDir && dirPath === "") {
|
|
4989
|
+
const effectivePath = currentDir && (currentDir === rootDir || currentDir.startsWith(rootDir + "/"))
|
|
4990
|
+
? currentDir
|
|
4991
|
+
: rootDir;
|
|
4992
|
+
selectedPath = effectivePath;
|
|
4993
|
+
refreshBreadcrumb(effectivePath);
|
|
4994
|
+
history[historyIndex] = effectivePath;
|
|
4995
|
+
}
|
|
4996
|
+
listContainer.innerHTML = "";
|
|
4785
4997
|
if (dirs.length === 0) {
|
|
4786
|
-
|
|
4998
|
+
listContainer.innerHTML = `<div class="dp-empty">${t("sib.dir.empty", "空目录")}</div>`;
|
|
4787
4999
|
} else {
|
|
4788
|
-
const parentNode = children.parentElement;
|
|
4789
|
-
const parentDepth = parseInt(parentNode?.dataset?.depth) || 0;
|
|
4790
|
-
const childDepth = parentDepth + 1;
|
|
4791
5000
|
const frag = document.createDocumentFragment();
|
|
4792
|
-
dirs.forEach(d => frag.appendChild(buildDirNode(d,
|
|
4793
|
-
|
|
4794
|
-
}
|
|
5001
|
+
dirs.forEach(d => frag.appendChild(buildDirNode(d, 0)));
|
|
5002
|
+
listContainer.appendChild(frag);
|
|
5003
|
+
}
|
|
4795
5004
|
} catch (err) {
|
|
4796
5005
|
console.error("dir picker load failed:", err);
|
|
4797
|
-
|
|
5006
|
+
listContainer.innerHTML = `<div class="dp-error">${t("sib.dir.loadError", "加载失败")}</div>`;
|
|
4798
5007
|
}
|
|
4799
5008
|
}
|
|
4800
5009
|
|
|
4801
|
-
//
|
|
4802
|
-
|
|
4803
|
-
|
|
5010
|
+
// Back / Forward
|
|
5011
|
+
backBtn.addEventListener("click", () => {
|
|
5012
|
+
if (historyIndex <= 0) return;
|
|
5013
|
+
historyIndex--;
|
|
5014
|
+
refreshNavBtns();
|
|
5015
|
+
navigateTo(history[historyIndex], true, false);
|
|
5016
|
+
});
|
|
5017
|
+
fwdBtn.addEventListener("click", () => {
|
|
5018
|
+
if (historyIndex >= history.length - 1) return;
|
|
5019
|
+
historyIndex++;
|
|
5020
|
+
refreshNavBtns();
|
|
5021
|
+
navigateTo(history[historyIndex], true, false);
|
|
5022
|
+
});
|
|
4804
5023
|
|
|
4805
|
-
//
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
modal.style.maxHeight = "80vh";
|
|
4810
|
-
modal.style.display = "flex";
|
|
4811
|
-
modal.style.flexDirection = "column";
|
|
4812
|
-
|
|
4813
|
-
// Title
|
|
4814
|
-
const title = document.createElement("div");
|
|
4815
|
-
title.className = "modal-title";
|
|
4816
|
-
title.textContent = sessionLess
|
|
4817
|
-
? t("sessions.modal.dirpicker.title", "选择工作目录")
|
|
4818
|
-
: t("sib.dir.changePrompt", "切换工作目录");
|
|
4819
|
-
modal.appendChild(title);
|
|
4820
|
-
|
|
4821
|
-
// Modal body
|
|
4822
|
-
const body = document.createElement("div");
|
|
4823
|
-
body.className = "modal-body";
|
|
4824
|
-
body.style.flex = "1";
|
|
4825
|
-
body.style.overflow = "hidden";
|
|
4826
|
-
body.style.display = "flex";
|
|
4827
|
-
body.style.flexDirection = "column";
|
|
4828
|
-
body.style.gap = "8px";
|
|
4829
|
-
|
|
4830
|
-
// Quick presets (will be populated with absolute paths after first API call)
|
|
4831
|
-
const presets = document.createElement("div");
|
|
4832
|
-
presets.className = "dp-presets";
|
|
4833
|
-
body.appendChild(presets);
|
|
4834
|
-
|
|
4835
|
-
// "Up one level" button — navigates to the parent of the current path.
|
|
4836
|
-
const upBtn = document.createElement("button");
|
|
4837
|
-
upBtn.className = "btn btn-secondary btn-sm dp-up-btn";
|
|
4838
|
-
upBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg><span>${t("sib.dir.up", "上一级")}</span>`;
|
|
4839
|
-
function parentOf(p) {
|
|
4840
|
-
const trimmed = (p || "").replace(/\/+$/, "");
|
|
4841
|
-
if (!trimmed || trimmed === "/" ) return null;
|
|
4842
|
-
const idx = trimmed.lastIndexOf("/");
|
|
4843
|
-
if (idx < 0) return null;
|
|
4844
|
-
return idx === 0 ? "/" : trimmed.substring(0, idx);
|
|
4845
|
-
}
|
|
4846
|
-
function refreshUpBtn() {
|
|
4847
|
-
const parent = parentOf(pathInput.value);
|
|
4848
|
-
upBtn.disabled = !parent;
|
|
4849
|
-
}
|
|
4850
|
-
upBtn.addEventListener("click", () => {
|
|
4851
|
-
const parent = parentOf(pathInput.value);
|
|
4852
|
-
if (!parent) return;
|
|
4853
|
-
selectedPath = parent;
|
|
4854
|
-
pathInput.value = parent;
|
|
4855
|
-
modal.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
4856
|
-
loadTreeForPath(parent, true);
|
|
5024
|
+
// Hidden files toggle
|
|
5025
|
+
hiddenCheckbox.addEventListener("change", () => {
|
|
5026
|
+
showHidden = hiddenCheckbox.checked;
|
|
5027
|
+
navigateTo(selectedPath || currentDir, true);
|
|
4857
5028
|
});
|
|
4858
5029
|
|
|
4859
|
-
//
|
|
4860
|
-
// Drops an editable placeholder row at the top of the current view
|
|
4861
|
-
// and POSTs to the backend on commit. Confirm = Enter / blur, cancel
|
|
4862
|
-
// = Escape. The placeholder is purely visual; nothing is created on
|
|
4863
|
-
// disk until the user commits a non-empty name.
|
|
4864
|
-
const newFolderBtn = document.createElement("button");
|
|
4865
|
-
newFolderBtn.className = "btn btn-secondary btn-sm dp-newfolder-btn";
|
|
4866
|
-
newFolderBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span>${t("sib.dir.newFolder", "新建文件夹")}</span>`;
|
|
5030
|
+
// New folder — inserts an editable placeholder row at the top of the list
|
|
4867
5031
|
newFolderBtn.addEventListener("click", () => beginCreateFolder());
|
|
4868
5032
|
|
|
4869
5033
|
function beginCreateFolder() {
|
|
4870
|
-
const parentDir = (
|
|
5034
|
+
const parentDir = (selectedPath || "").trim();
|
|
4871
5035
|
if (!parentDir.startsWith("/")) {
|
|
4872
5036
|
alert(t("sib.dir.mkdirError", "Failed to create folder: {{msg}}").replace("{{msg}}", "invalid parent path"));
|
|
4873
5037
|
return;
|
|
4874
5038
|
}
|
|
4875
5039
|
|
|
4876
|
-
// Locate the parent node in the current tree, if it's rendered.
|
|
4877
|
-
// The path input may point at any expanded descendant of the
|
|
4878
|
-
// tree root, so we can't assume parentDir == tree root.
|
|
4879
|
-
// Walk every rendered .dp-node and match by entry.absPath.
|
|
4880
|
-
let parentNode = null;
|
|
4881
|
-
treeContainer.querySelectorAll(".dp-node").forEach(n => {
|
|
4882
|
-
if (parentNode) return;
|
|
4883
|
-
if (n._dpEntry && n._dpEntry.absPath === parentDir) parentNode = n;
|
|
4884
|
-
});
|
|
4885
|
-
|
|
4886
|
-
// Pick where to insert + what depth the placeholder lives at.
|
|
4887
|
-
// - Tree root (parentDir is the root) → depth 0, host = treeContainer
|
|
4888
|
-
// - Rendered subtree → depth = parent.depth + 1,
|
|
4889
|
-
// host = parent's .dp-children
|
|
4890
|
-
let host, depth, parentRow, parentCaret, parentChildren;
|
|
4891
|
-
if (parentNode) {
|
|
4892
|
-
parentRow = parentNode._dpRow;
|
|
4893
|
-
parentCaret = parentRow.querySelector(".dp-caret");
|
|
4894
|
-
parentChildren = parentNode.querySelector(":scope > .dp-children");
|
|
4895
|
-
depth = (parseInt(parentNode.dataset.depth, 10) || 0) + 1;
|
|
4896
|
-
host = parentChildren;
|
|
4897
|
-
} else {
|
|
4898
|
-
depth = 0;
|
|
4899
|
-
host = treeContainer;
|
|
4900
|
-
}
|
|
4901
|
-
|
|
4902
|
-
// Build a placeholder node and seed it with an inline editor.
|
|
4903
5040
|
const placeholder = document.createElement("div");
|
|
4904
5041
|
placeholder.className = "dp-node dp-node-pending";
|
|
4905
|
-
placeholder.dataset.depth = depth;
|
|
4906
5042
|
|
|
4907
5043
|
const row = document.createElement("div");
|
|
4908
5044
|
row.className = "dp-row dp-row-editing";
|
|
4909
|
-
row.style.paddingLeft = `${depth * 16 + 8}px`;
|
|
4910
|
-
|
|
4911
|
-
const caret = document.createElement("span");
|
|
4912
|
-
caret.className = "dp-caret";
|
|
4913
|
-
caret.innerHTML = ICON_CARET_SVG;
|
|
4914
5045
|
|
|
4915
5046
|
const icon = document.createElement("span");
|
|
4916
5047
|
icon.className = "dp-icon";
|
|
@@ -4919,29 +5050,14 @@ const Sessions = (() => {
|
|
|
4919
5050
|
const nameSpan = document.createElement("span");
|
|
4920
5051
|
nameSpan.className = "dp-name";
|
|
4921
5052
|
|
|
4922
|
-
row.appendChild(caret);
|
|
4923
5053
|
row.appendChild(icon);
|
|
4924
5054
|
row.appendChild(nameSpan);
|
|
4925
5055
|
placeholder.appendChild(row);
|
|
4926
5056
|
|
|
4927
|
-
//
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
if (parentNode) {
|
|
4932
|
-
parentCaret.classList.add("open");
|
|
4933
|
-
parentChildren.style.display = "flex";
|
|
4934
|
-
// Drop "loading" / "empty" / "error" stub if present.
|
|
4935
|
-
const stub = parentChildren.querySelector(".dp-empty, .dp-loading, .dp-error");
|
|
4936
|
-
if (stub) stub.remove();
|
|
4937
|
-
parentChildren.dataset.loaded = "1";
|
|
4938
|
-
parentChildren.insertBefore(placeholder, parentChildren.firstChild);
|
|
4939
|
-
} else {
|
|
4940
|
-
// Root level: drop the empty/loading/error stub, then prepend.
|
|
4941
|
-
const emptyEl = treeContainer.querySelector(":scope > .dp-empty, :scope > .dp-loading, :scope > .dp-error");
|
|
4942
|
-
if (emptyEl) emptyEl.remove();
|
|
4943
|
-
treeContainer.insertBefore(placeholder, treeContainer.firstChild);
|
|
4944
|
-
}
|
|
5057
|
+
// Drop stubs and prepend
|
|
5058
|
+
const emptyEl = listContainer.querySelector(".dp-empty, .dp-loading, .dp-error");
|
|
5059
|
+
if (emptyEl) emptyEl.remove();
|
|
5060
|
+
listContainer.insertBefore(placeholder, listContainer.firstChild);
|
|
4945
5061
|
|
|
4946
5062
|
startInlineEdit(nameSpan, t("sib.dir.newFolderDefault", "New Folder"), {
|
|
4947
5063
|
onCommit: async (newName) => {
|
|
@@ -4960,8 +5076,6 @@ const Sessions = (() => {
|
|
|
4960
5076
|
placeholder.remove();
|
|
4961
5077
|
return;
|
|
4962
5078
|
}
|
|
4963
|
-
// Replace placeholder with a real, fully-wired node so it can
|
|
4964
|
-
// be expanded / right-clicked exactly like any sibling.
|
|
4965
5079
|
const realEntry = {
|
|
4966
5080
|
name: data.name,
|
|
4967
5081
|
path: data.path,
|
|
@@ -4969,14 +5083,12 @@ const Sessions = (() => {
|
|
|
4969
5083
|
absolute: true,
|
|
4970
5084
|
type: "dir"
|
|
4971
5085
|
};
|
|
4972
|
-
const realNode = buildDirNode(realEntry,
|
|
5086
|
+
const realNode = buildDirNode(realEntry, 0);
|
|
4973
5087
|
placeholder.replaceWith(realNode);
|
|
4974
|
-
|
|
4975
|
-
modal.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
5088
|
+
listContainer.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
4976
5089
|
realNode._dpRow.classList.add("selected");
|
|
4977
5090
|
selectedPath = data.path;
|
|
4978
|
-
|
|
4979
|
-
refreshUpBtn();
|
|
5091
|
+
refreshBreadcrumb(data.path);
|
|
4980
5092
|
} catch (err) {
|
|
4981
5093
|
alert(t("sib.dir.mkdirError", "Failed to create folder: {{msg}}").replace("{{msg}}", err.message || String(err)));
|
|
4982
5094
|
placeholder.remove();
|
|
@@ -4986,10 +5098,7 @@ const Sessions = (() => {
|
|
|
4986
5098
|
});
|
|
4987
5099
|
}
|
|
4988
5100
|
|
|
4989
|
-
// ── Inline rename editor (shared by mkdir
|
|
4990
|
-
// Replaces a name span with an <input> seeded by `initialValue`,
|
|
4991
|
-
// commits on Enter/blur, cancels on Escape. The caller wires the
|
|
4992
|
-
// backend round-trip via onCommit (it receives the new name string).
|
|
5101
|
+
// ── Inline rename editor (shared by mkdir) ────────────────────────
|
|
4993
5102
|
function startInlineEdit(nameSpan, initialValue, { onCommit, onCancel }) {
|
|
4994
5103
|
const input = document.createElement("input");
|
|
4995
5104
|
input.type = "text";
|
|
@@ -4998,39 +5107,21 @@ const Sessions = (() => {
|
|
|
4998
5107
|
input.spellcheck = false;
|
|
4999
5108
|
input.autocomplete = "off";
|
|
5000
5109
|
input.setAttribute("autocapitalize", "off");
|
|
5001
|
-
|
|
5002
|
-
// Stop click-through so editing doesn't trigger row selection /
|
|
5003
|
-
// expand / collapse.
|
|
5004
5110
|
["click", "dblclick", "mousedown"].forEach(ev =>
|
|
5005
5111
|
input.addEventListener(ev, (e) => e.stopPropagation())
|
|
5006
5112
|
);
|
|
5007
|
-
|
|
5008
|
-
// Replace span with input. Track via parent so we can swap back.
|
|
5009
5113
|
const parent = nameSpan.parentNode;
|
|
5010
5114
|
parent.replaceChild(input, nameSpan);
|
|
5011
|
-
|
|
5012
|
-
// mutations (e.g. inserting a placeholder row) finish first.
|
|
5013
|
-
setTimeout(() => {
|
|
5014
|
-
input.focus();
|
|
5015
|
-
input.select();
|
|
5016
|
-
}, 0);
|
|
5017
|
-
|
|
5115
|
+
setTimeout(() => { input.focus(); input.select(); }, 0);
|
|
5018
5116
|
let finished = false;
|
|
5019
5117
|
const finish = (commit) => {
|
|
5020
5118
|
if (finished) return;
|
|
5021
5119
|
finished = true;
|
|
5022
5120
|
const value = input.value;
|
|
5023
5121
|
if (input.parentNode === parent) parent.replaceChild(nameSpan, input);
|
|
5024
|
-
if (commit) {
|
|
5025
|
-
|
|
5026
|
-
} else {
|
|
5027
|
-
try { onCancel && onCancel(); } catch (e) { console.error(e); }
|
|
5028
|
-
}
|
|
5122
|
+
if (commit) { try { onCommit && onCommit(value); } catch (e) { console.error(e); } }
|
|
5123
|
+
else { try { onCancel && onCancel(); } catch (e) { console.error(e); } }
|
|
5029
5124
|
};
|
|
5030
|
-
|
|
5031
|
-
// Enter commits; Escape cancels. Use IME.bindEnter so that the
|
|
5032
|
-
// Enter that confirms a Chinese / Japanese / Korean IME candidate
|
|
5033
|
-
// does NOT commit the rename. Mirrors Sessions._startRename.
|
|
5034
5125
|
IME.bindEnter(input, () => finish(true));
|
|
5035
5126
|
input.addEventListener("keydown", (e) => {
|
|
5036
5127
|
if (e.key === "Escape") { e.preventDefault(); finish(false); }
|
|
@@ -5038,341 +5129,31 @@ const Sessions = (() => {
|
|
|
5038
5129
|
input.addEventListener("blur", () => finish(true));
|
|
5039
5130
|
}
|
|
5040
5131
|
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
{ value: "/", text: t("sib.dir.root", "根目录"), absolute: true }
|
|
5049
|
-
]
|
|
5050
|
-
: [
|
|
5051
|
-
{ value: defaultDir, text: t("sib.dir.default", "默认工作目录"), absolute: true },
|
|
5052
|
-
{ value: "/", text: t("sib.dir.root", "根目录"), absolute: true }
|
|
5053
|
-
];
|
|
5054
|
-
presetDirs.forEach(p => {
|
|
5055
|
-
// Skip preset whose absolute path isn't known yet (defensive: the
|
|
5056
|
-
// backend always returns it, but a malformed deploy shouldn't render
|
|
5057
|
-
// a button that navigates to "").
|
|
5058
|
-
if (!p.value) return;
|
|
5059
|
-
const btn = document.createElement("button");
|
|
5060
|
-
btn.className = "btn btn-secondary btn-sm";
|
|
5061
|
-
btn.textContent = p.text;
|
|
5062
|
-
btn.addEventListener("click", () => {
|
|
5063
|
-
selectedPath = p.value;
|
|
5064
|
-
pathInput.value = p.value;
|
|
5065
|
-
modal.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
5066
|
-
loadTreeForPath(p.value, p.absolute);
|
|
5067
|
-
});
|
|
5068
|
-
presets.appendChild(btn);
|
|
5069
|
-
});
|
|
5070
|
-
// "+ New folder" trailing action on the same toolbar.
|
|
5071
|
-
presets.appendChild(newFolderBtn);
|
|
5072
|
-
}
|
|
5073
|
-
|
|
5074
|
-
// Path input
|
|
5075
|
-
const pathContainer = document.createElement("div");
|
|
5076
|
-
pathContainer.className = "dp-path-container";
|
|
5077
|
-
const pathInput = document.createElement("input");
|
|
5078
|
-
pathInput.type = "text";
|
|
5079
|
-
pathInput.className = "dir-picker-input";
|
|
5080
|
-
pathInput.spellcheck = false;
|
|
5081
|
-
pathInput.autocomplete = "off";
|
|
5082
|
-
pathInput.setAttribute("autocapitalize", "off");
|
|
5083
|
-
pathInput.value = currentDir;
|
|
5084
|
-
pathInput.placeholder = t("sib.dir.inputPlaceholder", "输入或选择目录路径");
|
|
5085
|
-
pathContainer.appendChild(pathInput);
|
|
5086
|
-
|
|
5087
|
-
// Autocomplete dropdown
|
|
5088
|
-
const autocomplete = document.createElement("div");
|
|
5089
|
-
autocomplete.className = "dp-autocomplete";
|
|
5090
|
-
autocomplete.style.display = "none";
|
|
5091
|
-
pathContainer.appendChild(autocomplete);
|
|
5092
|
-
|
|
5093
|
-
body.appendChild(pathContainer);
|
|
5094
|
-
|
|
5095
|
-
// Show hidden files toggle
|
|
5096
|
-
const hiddenToggle = document.createElement("label");
|
|
5097
|
-
hiddenToggle.className = "dp-hidden-toggle";
|
|
5098
|
-
const hiddenCheckbox = document.createElement("input");
|
|
5099
|
-
hiddenCheckbox.type = "checkbox";
|
|
5100
|
-
hiddenCheckbox.checked = false;
|
|
5101
|
-
const hiddenLabelText = document.createElement("span");
|
|
5102
|
-
hiddenLabelText.textContent = t("sib.dir.showHidden", "显示隐藏文件");
|
|
5103
|
-
hiddenToggle.appendChild(hiddenCheckbox);
|
|
5104
|
-
hiddenToggle.appendChild(hiddenLabelText);
|
|
5105
|
-
body.appendChild(hiddenToggle);
|
|
5106
|
-
|
|
5107
|
-
// Tree container
|
|
5108
|
-
const treeContainer = document.createElement("div");
|
|
5109
|
-
treeContainer.className = "dp-tree";
|
|
5110
|
-
treeContainer.style.flex = "1";
|
|
5111
|
-
treeContainer.style.overflow = "auto";
|
|
5112
|
-
treeContainer.innerHTML = `<div class="dp-loading">${t("sib.dir.loading", "加载中...")}</div>`;
|
|
5113
|
-
body.appendChild(treeContainer);
|
|
5114
|
-
|
|
5115
|
-
modal.appendChild(body);
|
|
5116
|
-
|
|
5117
|
-
// Buttons
|
|
5118
|
-
const buttonContainer = document.createElement("div");
|
|
5119
|
-
buttonContainer.className = "modal-buttons";
|
|
5120
|
-
|
|
5121
|
-
const cancelButton = document.createElement("button");
|
|
5122
|
-
cancelButton.className = "btn btn-secondary";
|
|
5123
|
-
cancelButton.textContent = t("sib.dir.cancel", "取消");
|
|
5124
|
-
cancelButton.onclick = () => {
|
|
5125
|
-
overlay.remove();
|
|
5126
|
-
resolve(null);
|
|
5127
|
-
};
|
|
5128
|
-
|
|
5129
|
-
const confirmButton = document.createElement("button");
|
|
5130
|
-
confirmButton.className = "btn btn-primary";
|
|
5131
|
-
confirmButton.textContent = t("sib.dir.confirm", "确认");
|
|
5132
|
-
confirmButton.onclick = () => {
|
|
5133
|
-
const dir = pathInput.value.trim();
|
|
5134
|
-
overlay.remove();
|
|
5135
|
-
resolve(dir || null);
|
|
5136
|
-
};
|
|
5137
|
-
|
|
5138
|
-
buttonContainer.appendChild(cancelButton);
|
|
5139
|
-
buttonContainer.appendChild(confirmButton);
|
|
5140
|
-
modal.appendChild(buttonContainer);
|
|
5141
|
-
|
|
5142
|
-
overlay.appendChild(modal);
|
|
5143
|
-
document.body.appendChild(overlay);
|
|
5144
|
-
|
|
5145
|
-
// Sync pathInput changes to selectedPath
|
|
5146
|
-
pathInput.addEventListener("input", () => {
|
|
5147
|
-
selectedPath = pathInput.value;
|
|
5148
|
-
modal.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
5149
|
-
refreshUpBtn();
|
|
5150
|
-
});
|
|
5151
|
-
|
|
5152
|
-
// Load tree for a given path
|
|
5153
|
-
async function loadTreeForPath(dirPath, absolute = false) {
|
|
5154
|
-
treeContainer.innerHTML = `<div class="dp-loading">${t("sib.dir.loading", "加载中...")}</div>`;
|
|
5155
|
-
if (dirPath) { pathInput.value = dirPath; selectedPath = dirPath; }
|
|
5156
|
-
// Session-less mode browses absolute paths directly via /api/dirs;
|
|
5157
|
-
// skip the working-directory-relative path math.
|
|
5158
|
-
const useAbsolute = sessionLess || absolute || (dirPath.startsWith("/") && (!rootDir || !(dirPath === rootDir || dirPath.startsWith(rootDir + "/"))));
|
|
5159
|
-
// Convert absolute path to relative path for API
|
|
5160
|
-
let relPath = dirPath;
|
|
5161
|
-
if (!sessionLess && !useAbsolute && rootDir && (dirPath === rootDir || dirPath.startsWith(rootDir + "/"))) {
|
|
5162
|
-
relPath = dirPath.substring(rootDir.length).replace(/^\/+/, "");
|
|
5163
|
-
}
|
|
5164
|
-
try {
|
|
5165
|
-
const dirs = await fetchDirs(relPath, useAbsolute);
|
|
5166
|
-
// Update presets and pathInput with absolute paths after first API call
|
|
5167
|
-
if (rootDir && presets.children.length === 0) {
|
|
5168
|
-
setupPresets();
|
|
5169
|
-
// Update pathInput to show absolute path
|
|
5170
|
-
if (rootDir !== currentDir && !(currentDir === rootDir || currentDir.startsWith(rootDir + "/"))) {
|
|
5171
|
-
pathInput.value = rootDir;
|
|
5172
|
-
selectedPath = rootDir;
|
|
5173
|
-
}
|
|
5174
|
-
}
|
|
5175
|
-
treeContainer.innerHTML = "";
|
|
5176
|
-
if (dirs.length === 0) {
|
|
5177
|
-
treeContainer.innerHTML = `<div class="dp-empty">${t("sib.dir.empty", "空目录")}</div>`;
|
|
5178
|
-
} else {
|
|
5179
|
-
const frag = document.createDocumentFragment();
|
|
5180
|
-
dirs.forEach(d => frag.appendChild(buildDirNode(d, 0)));
|
|
5181
|
-
treeContainer.appendChild(frag);
|
|
5182
|
-
}
|
|
5183
|
-
refreshUpBtn();
|
|
5184
|
-
} catch (err) {
|
|
5185
|
-
console.error("dir picker load failed:", err);
|
|
5186
|
-
treeContainer.innerHTML = `<div class="dp-error">${t("sib.dir.loadError", "加载失败")}</div>`;
|
|
5187
|
-
}
|
|
5188
|
-
}
|
|
5189
|
-
// Session-less mode: start browsing from the requested default (e.g.
|
|
5190
|
-
// ~/clacky_workspace). The backend walks up to the nearest existing
|
|
5191
|
-
// ancestor if it doesn't exist yet, while pathInput keeps the original.
|
|
5192
|
-
if (sessionLess && currentDir) {
|
|
5193
|
-
const wanted = currentDir;
|
|
5194
|
-
loadTreeForPath(currentDir, true).then(() => {
|
|
5195
|
-
pathInput.value = wanted;
|
|
5196
|
-
selectedPath = wanted;
|
|
5197
|
-
refreshUpBtn();
|
|
5198
|
-
});
|
|
5132
|
+
// ── Initial load ───────────────────────────────────────────────────
|
|
5133
|
+
// session-less: browse absolute paths from currentDir (or "/" fallback)
|
|
5134
|
+
// sessionful: load relative root first so rootDir is populated, then
|
|
5135
|
+
// navigate to currentDir if it's provided.
|
|
5136
|
+
if (sessionLess) {
|
|
5137
|
+
const startDir = currentDir || "/";
|
|
5138
|
+
navigateTo(startDir, true);
|
|
5199
5139
|
} else {
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
// ── Autocomplete logic ──────────────────────────────────────────────
|
|
5211
|
-
let autocompleteTimer = null;
|
|
5212
|
-
let activeIndex = -1;
|
|
5213
|
-
|
|
5214
|
-
function hideAutocomplete() {
|
|
5215
|
-
autocomplete.style.display = "none";
|
|
5216
|
-
autocomplete.innerHTML = "";
|
|
5217
|
-
activeIndex = -1;
|
|
5218
|
-
}
|
|
5219
|
-
|
|
5220
|
-
function showAutocomplete(items) {
|
|
5221
|
-
autocomplete.innerHTML = "";
|
|
5222
|
-
if (!items.length) { hideAutocomplete(); return; }
|
|
5223
|
-
|
|
5224
|
-
items.forEach((item, i) => {
|
|
5225
|
-
const row = document.createElement("div");
|
|
5226
|
-
row.className = "dp-ac-item";
|
|
5227
|
-
if (i === activeIndex) row.classList.add("active");
|
|
5228
|
-
|
|
5229
|
-
const icon = document.createElement("span");
|
|
5230
|
-
icon.className = "dp-ac-icon";
|
|
5231
|
-
icon.innerHTML = ICON_FOLDER_SVG;
|
|
5232
|
-
|
|
5233
|
-
const name = document.createElement("span");
|
|
5234
|
-
name.className = "dp-ac-name";
|
|
5235
|
-
name.textContent = item.name;
|
|
5236
|
-
|
|
5237
|
-
row.appendChild(icon);
|
|
5238
|
-
row.appendChild(name);
|
|
5239
|
-
|
|
5240
|
-
row.addEventListener("mousedown", (e) => {
|
|
5241
|
-
e.preventDefault(); // prevent blur
|
|
5242
|
-
// Construct full path: keep parent path from input, append selected item name
|
|
5243
|
-
const inputVal = pathInput.value;
|
|
5244
|
-
const lastSlash = inputVal.lastIndexOf("/");
|
|
5245
|
-
const fullPath = lastSlash >= 0
|
|
5246
|
-
? inputVal.substring(0, lastSlash + 1) + item.name
|
|
5247
|
-
: item.name;
|
|
5248
|
-
pathInput.value = fullPath;
|
|
5249
|
-
selectedPath = fullPath;
|
|
5250
|
-
hideAutocomplete();
|
|
5251
|
-
loadTreeForPath(fullPath);
|
|
5252
|
-
});
|
|
5253
|
-
row.addEventListener("mouseenter", () => {
|
|
5254
|
-
activeIndex = i;
|
|
5255
|
-
autocomplete.querySelectorAll(".dp-ac-item").forEach((el, j) => {
|
|
5256
|
-
el.classList.toggle("active", j === i);
|
|
5257
|
-
});
|
|
5258
|
-
});
|
|
5259
|
-
|
|
5260
|
-
autocomplete.appendChild(row);
|
|
5261
|
-
});
|
|
5262
|
-
autocomplete.style.display = "";
|
|
5263
|
-
}
|
|
5264
|
-
|
|
5265
|
-
async function fetchSuggestions(inputVal) {
|
|
5266
|
-
if (!inputVal) { hideAutocomplete(); return; }
|
|
5267
|
-
|
|
5268
|
-
// Determine parent dir and prefix
|
|
5269
|
-
const lastSlash = inputVal.lastIndexOf("/");
|
|
5270
|
-
let parentPath, prefix;
|
|
5271
|
-
if (lastSlash > 0) {
|
|
5272
|
-
parentPath = inputVal.substring(0, lastSlash);
|
|
5273
|
-
prefix = inputVal.substring(lastSlash + 1).toLowerCase();
|
|
5274
|
-
} else if (lastSlash === 0) {
|
|
5275
|
-
parentPath = "/";
|
|
5276
|
-
prefix = inputVal.substring(1).toLowerCase();
|
|
5277
|
-
} else {
|
|
5278
|
-
parentPath = "";
|
|
5279
|
-
prefix = inputVal.toLowerCase();
|
|
5280
|
-
}
|
|
5281
|
-
|
|
5282
|
-
// Determine if we need absolute mode (path outside working directory)
|
|
5283
|
-
const isAbsolute = parentPath.startsWith("/") && (!rootDir || !(parentPath === rootDir || parentPath.startsWith(rootDir + "/")));
|
|
5284
|
-
let relPath = parentPath;
|
|
5285
|
-
if (!isAbsolute && rootDir && (parentPath === rootDir || parentPath.startsWith(rootDir + "/"))) {
|
|
5286
|
-
relPath = parentPath.substring(rootDir.length).replace(/^\/+/, "");
|
|
5287
|
-
}
|
|
5288
|
-
|
|
5289
|
-
try {
|
|
5290
|
-
const dirs = await fetchDirs(relPath, isAbsolute);
|
|
5291
|
-
const filtered = prefix
|
|
5292
|
-
? dirs.filter(d => d.name.toLowerCase().startsWith(prefix))
|
|
5293
|
-
: dirs;
|
|
5294
|
-
showAutocomplete(filtered.slice(0, 15)); // limit to 15 items
|
|
5295
|
-
} catch (_) {
|
|
5296
|
-
hideAutocomplete();
|
|
5297
|
-
}
|
|
5298
|
-
}
|
|
5299
|
-
|
|
5300
|
-
pathInput.addEventListener("input", () => {
|
|
5301
|
-
selectedPath = pathInput.value;
|
|
5302
|
-
modal.querySelectorAll(".dp-row.selected").forEach(el => el.classList.remove("selected"));
|
|
5303
|
-
clearTimeout(autocompleteTimer);
|
|
5304
|
-
autocompleteTimer = setTimeout(() => fetchSuggestions(pathInput.value.trim()), 200);
|
|
5305
|
-
});
|
|
5306
|
-
|
|
5307
|
-
pathInput.addEventListener("blur", () => {
|
|
5308
|
-
// Delay to allow mousedown on suggestion
|
|
5309
|
-
setTimeout(hideAutocomplete, 150);
|
|
5310
|
-
});
|
|
5311
|
-
|
|
5312
|
-
pathInput.addEventListener("focus", () => {
|
|
5313
|
-
if (pathInput.value.trim()) fetchSuggestions(pathInput.value.trim());
|
|
5314
|
-
});
|
|
5315
|
-
|
|
5316
|
-
// Keyboard navigation in autocomplete
|
|
5317
|
-
const pathIme = IME.track(pathInput);
|
|
5318
|
-
pathInput.addEventListener("keydown", (e) => {
|
|
5319
|
-
const items = autocomplete.querySelectorAll(".dp-ac-item");
|
|
5320
|
-
if (!items.length || autocomplete.style.display === "none") {
|
|
5321
|
-
if (e.key === "Enter") {
|
|
5322
|
-
if (pathIme.isComposing(e)) return;
|
|
5323
|
-
e.preventDefault();
|
|
5324
|
-
// Navigate to typed path without closing modal
|
|
5325
|
-
const dir = pathInput.value.trim();
|
|
5326
|
-
if (dir) {
|
|
5327
|
-
selectedPath = dir;
|
|
5328
|
-
hideAutocomplete();
|
|
5329
|
-
loadTreeForPath(dir);
|
|
5330
|
-
}
|
|
5331
|
-
}
|
|
5332
|
-
return;
|
|
5333
|
-
}
|
|
5334
|
-
|
|
5335
|
-
if (e.key === "ArrowDown") {
|
|
5336
|
-
e.preventDefault();
|
|
5337
|
-
activeIndex = Math.min(activeIndex + 1, items.length - 1);
|
|
5338
|
-
items.forEach((el, i) => el.classList.toggle("active", i === activeIndex));
|
|
5339
|
-
items[activeIndex]?.scrollIntoView({ block: "nearest" });
|
|
5340
|
-
} else if (e.key === "ArrowUp") {
|
|
5341
|
-
e.preventDefault();
|
|
5342
|
-
activeIndex = Math.max(activeIndex - 1, 0);
|
|
5343
|
-
items.forEach((el, i) => el.classList.toggle("active", i === activeIndex));
|
|
5344
|
-
items[activeIndex]?.scrollIntoView({ block: "nearest" });
|
|
5345
|
-
} else if (e.key === "Enter") {
|
|
5346
|
-
if (pathIme.isComposing(e)) return;
|
|
5347
|
-
e.preventDefault();
|
|
5348
|
-
if (activeIndex >= 0 && items[activeIndex]) {
|
|
5349
|
-
// Select the highlighted suggestion
|
|
5350
|
-
const evt = new MouseEvent("mousedown", { bubbles: true });
|
|
5351
|
-
items[activeIndex].dispatchEvent(evt);
|
|
5352
|
-
} else {
|
|
5353
|
-
// Navigate to typed path without closing modal
|
|
5354
|
-
const dir = pathInput.value.trim();
|
|
5355
|
-
if (dir) {
|
|
5356
|
-
selectedPath = dir;
|
|
5357
|
-
hideAutocomplete();
|
|
5358
|
-
loadTreeForPath(dir);
|
|
5140
|
+
// First load relative root to get rootDir from API, then optionally
|
|
5141
|
+
// re-navigate to the actual currentDir.
|
|
5142
|
+
navigateTo("", false).then ? navigateTo("", false) : navigateTo("", false);
|
|
5143
|
+
if (currentDir) {
|
|
5144
|
+
// After rootDir is known, breadcrumb + selectedPath already set by navigateTo.
|
|
5145
|
+
// If currentDir differs from rootDir, do a second navigate.
|
|
5146
|
+
setTimeout(() => {
|
|
5147
|
+
if (currentDir && currentDir !== rootDir) {
|
|
5148
|
+
navigateTo(currentDir, currentDir.startsWith("/"));
|
|
5359
5149
|
}
|
|
5360
|
-
}
|
|
5361
|
-
} else if (e.key === "Escape") {
|
|
5362
|
-
hideAutocomplete();
|
|
5150
|
+
}, 100);
|
|
5363
5151
|
}
|
|
5364
|
-
}
|
|
5152
|
+
}
|
|
5365
5153
|
|
|
5366
5154
|
overlay.addEventListener("keydown", (e) => {
|
|
5367
|
-
if (e.key === "Escape")
|
|
5368
|
-
if (autocomplete.style.display !== "none") {
|
|
5369
|
-
hideAutocomplete();
|
|
5370
|
-
} else {
|
|
5371
|
-
cancelButton.click();
|
|
5372
|
-
}
|
|
5373
|
-
}
|
|
5155
|
+
if (e.key === "Escape") cancelButton.click();
|
|
5374
5156
|
});
|
|
5375
|
-
|
|
5376
5157
|
overlay.addEventListener("click", (e) => {
|
|
5377
5158
|
if (e.target === overlay) cancelButton.click();
|
|
5378
5159
|
});
|