openclacky 1.5.13 → 1.5.15
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/CHANGELOG.md +96 -0
- data/lib/clacky/agent/chunk_index.rb +83 -0
- data/lib/clacky/agent/history_navigation.rb +239 -0
- data/lib/clacky/agent/session_serializer.rb +53 -98
- data/lib/clacky/agent.rb +477 -297
- data/lib/clacky/agent_config.rb +5 -3
- data/lib/clacky/billing/billing_store.rb +2 -2
- data/lib/clacky/billing/platform_billing.rb +7 -0
- data/lib/clacky/brand_config.rb +27 -19
- data/lib/clacky/cli.rb +92 -12
- data/lib/clacky/cli_guidance.rb +48 -0
- data/lib/clacky/client.rb +2 -2
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +307 -98
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
- data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
- data/lib/clacky/default_extensions/preview/ext.yml +20 -0
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
- data/lib/clacky/extension/api_extension.rb +18 -6
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/json_ui_controller.rb +4 -2
- data/lib/clacky/media/openai_compat.rb +70 -30
- data/lib/clacky/message_format/bedrock.rb +6 -1
- data/lib/clacky/message_format/open_ai.rb +22 -5
- data/lib/clacky/message_format/open_ai_responses.rb +7 -3
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/providers.rb +44 -13
- data/lib/clacky/rich_ui/components/composer_guidance.rb +49 -0
- data/lib/clacky/rich_ui/rich_ui_controller.rb +24 -4
- data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +13 -0
- data/lib/clacky/search_config.rb +3 -3
- data/lib/clacky/server/channel/channel_manager.rb +25 -7
- data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
- data/lib/clacky/server/dir_picker.rb +67 -4
- data/lib/clacky/server/git_panel.rb +10 -2
- data/lib/clacky/server/http_server.rb +391 -75
- data/lib/clacky/server/preview.rb +351 -0
- data/lib/clacky/server/session_registry.rb +4 -0
- data/lib/clacky/server/web_ui_controller.rb +10 -5
- data/lib/clacky/session_manager.rb +52 -1
- data/lib/clacky/skill.rb +48 -0
- data/lib/clacky/tools/browser.rb +176 -19
- data/lib/clacky/tools/web_search.rb +91 -8
- data/lib/clacky/ui2/components/command_suggestions.rb +3 -1
- data/lib/clacky/ui2/components/input_area.rb +37 -4
- data/lib/clacky/ui2/components/modal_component.rb +35 -6
- data/lib/clacky/ui2/screen_buffer.rb +1 -0
- data/lib/clacky/ui2/ui_controller.rb +72 -12
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +19 -7
- data/lib/clacky/utils/mac_app_detector.rb +186 -0
- data/lib/clacky/utils/model_pricing.rb +118 -68
- data/lib/clacky/utils/windows_app_detector.rb +334 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +998 -168
- data/lib/clacky/web/app.js +19 -0
- data/lib/clacky/web/components/chat-navigator.js +489 -202
- data/lib/clacky/web/components/code-editor.js +191 -5
- data/lib/clacky/web/components/composer.js +3 -1
- data/lib/clacky/web/components/datepicker.js +19 -19
- data/lib/clacky/web/components/mentions.js +11 -14
- data/lib/clacky/web/components/model-picker.js +66 -23
- data/lib/clacky/web/components/quote-select.js +341 -0
- data/lib/clacky/web/core/aside.js +157 -8
- data/lib/clacky/web/core/ext.js +16 -0
- data/lib/clacky/web/features/billing/view.js +75 -21
- data/lib/clacky/web/features/extensions/store.js +4 -1
- data/lib/clacky/web/features/model-tester/store.js +37 -4
- data/lib/clacky/web/features/skills/store.js +18 -1
- data/lib/clacky/web/features/skills/view.js +128 -7
- data/lib/clacky/web/features/workspace/store.js +80 -7
- data/lib/clacky/web/features/workspace/view.js +474 -43
- data/lib/clacky/web/i18n.js +158 -23
- data/lib/clacky/web/index.html +57 -29
- data/lib/clacky/web/sessions.js +517 -127
- data/lib/clacky/web/settings.js +155 -6
- data/lib/clacky/web/utils.js +34 -0
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
- data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
- data/lib/clacky/web/vendor/codemirror/package.json +29 -0
- data/lib/clacky/web/ws-dispatcher.js +161 -4
- data/lib/clacky.rb +2 -0
- metadata +13 -1
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"use strict";
|
|
17
17
|
|
|
18
18
|
const WorkspaceView = (() => {
|
|
19
|
-
const t = (key) => (typeof I18n !== "undefined" ? I18n.t(key) : key);
|
|
19
|
+
const t = (key, vars) => (typeof I18n !== "undefined" ? I18n.t(key, vars) : key);
|
|
20
20
|
|
|
21
21
|
const ICON_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
22
22
|
const ICON_FOLDER_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>';
|
|
@@ -27,6 +27,11 @@ const WorkspaceView = (() => {
|
|
|
27
27
|
const ICON_SAVE = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>';
|
|
28
28
|
const ICON_EYE = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
29
29
|
const ICON_CLOSE = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>';
|
|
30
|
+
const ICON_CHEVRON_DOWN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
31
|
+
const ICON_EXTERNAL = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
|
|
32
|
+
const ICON_PANEL_LEFT = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>';
|
|
33
|
+
const ICON_ARROW_LEFT = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg>';
|
|
34
|
+
const ICON_COLUMNS = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg>';
|
|
30
35
|
|
|
31
36
|
function el(tag, cls) {
|
|
32
37
|
const d = document.createElement(tag);
|
|
@@ -192,7 +197,7 @@ const WorkspaceView = (() => {
|
|
|
192
197
|
|
|
193
198
|
// ── File tree ───────────────────────────────────────────────────────────
|
|
194
199
|
|
|
195
|
-
const treeState = { activePath: null, treeEl: null, indexPromise: null };
|
|
200
|
+
const treeState = { activePath: null, treeEl: null, indexPromise: null, loadPromise: null };
|
|
196
201
|
|
|
197
202
|
function highlightActiveRow() {
|
|
198
203
|
const tree = treeState.treeEl;
|
|
@@ -357,6 +362,94 @@ const WorkspaceView = (() => {
|
|
|
357
362
|
if (existing) existing.remove();
|
|
358
363
|
}
|
|
359
364
|
|
|
365
|
+
function showOpenWithMenu(anchorBtn, entry) {
|
|
366
|
+
closeOpenWithMenu();
|
|
367
|
+
|
|
368
|
+
const menu = el("div", "wt-context-menu session-context-menu wv-open-menu");
|
|
369
|
+
const appendAction = (iconSvg, label, onClick) => {
|
|
370
|
+
const item = el("div", "session-actions-menu-item");
|
|
371
|
+
const iconEl = el("span", "session-actions-menu-icon");
|
|
372
|
+
iconEl.innerHTML = iconSvg;
|
|
373
|
+
const labelEl = el("span", "session-actions-menu-label");
|
|
374
|
+
labelEl.textContent = label;
|
|
375
|
+
item.appendChild(iconEl);
|
|
376
|
+
item.appendChild(labelEl);
|
|
377
|
+
item.addEventListener("click", () => {
|
|
378
|
+
closeOpenWithMenu();
|
|
379
|
+
onClick();
|
|
380
|
+
});
|
|
381
|
+
menu.appendChild(item);
|
|
382
|
+
};
|
|
383
|
+
appendAction(ICON_FOLDER, t("workspace.revealInFinder"), () => revealFile(entry));
|
|
384
|
+
appendAction(ICON_DOWNLOAD, t("workspace.download"), () => downloadFile(entry));
|
|
385
|
+
|
|
386
|
+
document.body.appendChild(menu);
|
|
387
|
+
menu.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
|
388
|
+
menu.style.position = "fixed";
|
|
389
|
+
const fitMenu = () => {
|
|
390
|
+
const r = anchorBtn.getBoundingClientRect();
|
|
391
|
+
const mr = menu.getBoundingClientRect();
|
|
392
|
+
if (mr.right > window.innerWidth - 8) {
|
|
393
|
+
menu.style.left = "auto";
|
|
394
|
+
menu.style.right = "8px";
|
|
395
|
+
}
|
|
396
|
+
if (mr.bottom > window.innerHeight - 8) {
|
|
397
|
+
menu.style.top = (Math.max(8, r.top - mr.height - 4)) + "px";
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const r = anchorBtn.getBoundingClientRect();
|
|
401
|
+
menu.style.top = (r.bottom + 4) + "px";
|
|
402
|
+
menu.style.left = r.left + "px";
|
|
403
|
+
requestAnimationFrame(fitMenu);
|
|
404
|
+
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
document.addEventListener("click", closeOpenWithMenu, { once: true });
|
|
407
|
+
}, 0);
|
|
408
|
+
|
|
409
|
+
Promise.all([
|
|
410
|
+
Workspace.fetchOpenWithApps(entry),
|
|
411
|
+
Workspace.fetchDefaultApp(entry)
|
|
412
|
+
]).then(([apps, defaultApp]) => {
|
|
413
|
+
if (!document.body.contains(menu)) return;
|
|
414
|
+
if (apps.length) {
|
|
415
|
+
const frag = document.createDocumentFragment();
|
|
416
|
+
apps.forEach((app) => {
|
|
417
|
+
const isDefault = defaultApp && defaultApp.path === app.path;
|
|
418
|
+
const item = el("div", "session-actions-menu-item" + (isDefault ? " wv-open-menu-item--default" : ""));
|
|
419
|
+
if (isDefault) item.title = t("workspace.openDefault");
|
|
420
|
+
const iconEl = el("span", "session-actions-menu-icon wv-open-app-icon");
|
|
421
|
+
const img = document.createElement("img");
|
|
422
|
+
img.alt = "";
|
|
423
|
+
img.src = "/api/app-icon?path=" + encodeURIComponent(app.path);
|
|
424
|
+
img.addEventListener("error", () => { img.remove(); });
|
|
425
|
+
iconEl.appendChild(img);
|
|
426
|
+
const labelEl = el("span", "session-actions-menu-label");
|
|
427
|
+
labelEl.textContent = app.name;
|
|
428
|
+
item.appendChild(iconEl);
|
|
429
|
+
item.appendChild(labelEl);
|
|
430
|
+
item.addEventListener("click", async () => {
|
|
431
|
+
closeOpenWithMenu();
|
|
432
|
+
try {
|
|
433
|
+
await Workspace.openWithApp(entry, app.path);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
console.error("open_with failed:", err);
|
|
436
|
+
Modal.toast(t("workspace.openWithFailed") + ": " + err.message, "error");
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
frag.appendChild(item);
|
|
440
|
+
});
|
|
441
|
+
frag.appendChild(el("div", "wv-open-menu-separator"));
|
|
442
|
+
menu.insertBefore(frag, menu.firstChild);
|
|
443
|
+
fitMenu();
|
|
444
|
+
}
|
|
445
|
+
}).catch(() => { /* not macOS or scan failed — reveal/download still work */ });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function closeOpenWithMenu() {
|
|
449
|
+
const existing = document.querySelector(".wv-open-menu");
|
|
450
|
+
if (existing) existing.remove();
|
|
451
|
+
}
|
|
452
|
+
|
|
360
453
|
async function copyPath(entry) {
|
|
361
454
|
const fallback = Workspace.state.workingDir.replace(/\/+$/, "") + "/" + entry.path.replace(/^\/+/, "");
|
|
362
455
|
let target = fallback;
|
|
@@ -441,29 +534,86 @@ const WorkspaceView = (() => {
|
|
|
441
534
|
}
|
|
442
535
|
}
|
|
443
536
|
|
|
537
|
+
// Reveal a path in the tree: expand every parent folder on the way down and
|
|
538
|
+
// mark the file's row active, so a file opened from a chat link shows where it
|
|
539
|
+
// lives instead of staying hidden inside a collapsed folder. A level's rows
|
|
540
|
+
// only exist once its parent has been fetched, so the walk is sequential.
|
|
541
|
+
async function revealTreePath(relPath) {
|
|
542
|
+
const tree = treeState.treeEl;
|
|
543
|
+
if (!tree || !relPath) return;
|
|
544
|
+
// Files outside the working dir have no row to reveal.
|
|
545
|
+
if (relPath.startsWith("/") || relPath.startsWith("~") || /^[A-Za-z]:/.test(relPath)) return;
|
|
546
|
+
if (treeState.loadPromise) await treeState.loadPromise;
|
|
547
|
+
|
|
548
|
+
const parts = relPath.split("/").filter(Boolean);
|
|
549
|
+
for (let depth = 0; depth < parts.length - 1; depth++) {
|
|
550
|
+
const dirPath = parts.slice(0, depth + 1).join("/");
|
|
551
|
+
const row = tree.querySelector(`.wt-row[data-path="${CSS.escape(dirPath)}"]`);
|
|
552
|
+
const children = row && row.parentElement ? row.parentElement.querySelector(".wt-children") : null;
|
|
553
|
+
if (!children) return;
|
|
554
|
+
const caret = row.querySelector(".wt-caret");
|
|
555
|
+
if (!caret || caret.classList.contains("open")) continue;
|
|
556
|
+
await toggleDir(
|
|
557
|
+
{ name: parts[depth], path: dirPath, type: "dir" },
|
|
558
|
+
caret,
|
|
559
|
+
children,
|
|
560
|
+
row.querySelector(".wt-icon"),
|
|
561
|
+
_activeViewer && _activeViewer.openTab
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
setActiveFilePath(relPath);
|
|
565
|
+
}
|
|
566
|
+
|
|
444
567
|
// ── Inline tabbed viewer ────────────────────────────────────────────────
|
|
445
568
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
569
|
+
const VIEWER_ASIDE_WIDTH = 700;
|
|
570
|
+
|
|
571
|
+
// Active viewer instance (one per mounted Files tab). Kept at module level
|
|
572
|
+
// so openFile() — called from chat file:// link clicks — can reach the
|
|
573
|
+
// viewer without waiting for a DOM query or a custom event round-trip.
|
|
574
|
+
let _activeViewer = null;
|
|
575
|
+
// Unsubscriber for the active mount's refresh listeners. The aside can
|
|
576
|
+
// re-mount the Files tab without running the previous teardown (new-session
|
|
577
|
+
// page / same-session re-render), so the next mount releases these first to
|
|
578
|
+
// avoid stacking duplicate reloads.
|
|
579
|
+
let _activeOff = null;
|
|
580
|
+
|
|
581
|
+
// Binary formats the viewer cannot render are handed straight to the OS
|
|
582
|
+
// default application, so clicking a .docx link opens Word instead of parking
|
|
583
|
+
// the user on a "can't preview this" page with a button they still have to
|
|
584
|
+
// press. Extensions the system would run or mount rather than display stay
|
|
585
|
+
// behind the fallback page: clicking a link is not consent to run a program.
|
|
586
|
+
const NO_AUTO_OPEN_EXTS = new Set([
|
|
587
|
+
"exe", "msi", "dmg", "iso", "jar", "war", "app", "command",
|
|
588
|
+
"sh", "bash", "zsh", "bat", "cmd", "ps1", "scpt", "workflow",
|
|
589
|
+
"so", "dylib", "dll", "class", "pyc", "bin", "wasm",
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
// Only reached for entries CodeEditor classified as binary, so an extension
|
|
593
|
+
// missing here means the OS opens it in a viewer rather than running it.
|
|
594
|
+
function autoOpenWithSystem(filename) {
|
|
595
|
+
const ext = (filename || "").split(".").pop().toLowerCase();
|
|
596
|
+
return !!ext && !NO_AUTO_OPEN_EXTS.has(ext);
|
|
460
597
|
}
|
|
461
598
|
|
|
462
|
-
function createViewer(onTabsChanged) {
|
|
599
|
+
function createViewer(onTabsChanged, onToggleTree, onShowMobileTree) {
|
|
463
600
|
const tabs = [];
|
|
464
601
|
let activeId = null;
|
|
602
|
+
let releaseAsideWidth = null;
|
|
465
603
|
const notify = () => { if (onTabsChanged) onTabsChanged({ count: tabs.length, activeId }); };
|
|
466
604
|
|
|
605
|
+
function ensureAsideWidth() {
|
|
606
|
+
if (releaseAsideWidth || !window.Clacky || !Clacky.Aside || !Clacky.Aside.requestWidth) return;
|
|
607
|
+
releaseAsideWidth = Clacky.Aside.requestWidth(VIEWER_ASIDE_WIDTH);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function restoreAsideWidth() {
|
|
611
|
+
if (!releaseAsideWidth) return;
|
|
612
|
+
const release = releaseAsideWidth;
|
|
613
|
+
releaseAsideWidth = null;
|
|
614
|
+
release();
|
|
615
|
+
}
|
|
616
|
+
|
|
467
617
|
const root = el("div", "wv-viewer");
|
|
468
618
|
|
|
469
619
|
const tabbar = el("div", "wv-tabbar");
|
|
@@ -477,19 +627,63 @@ const WorkspaceView = (() => {
|
|
|
477
627
|
const btn = el("button", "wv-action" + (cls ? " " + cls : ""));
|
|
478
628
|
btn.type = "button";
|
|
479
629
|
btn.title = title;
|
|
630
|
+
btn.setAttribute("aria-label", title);
|
|
480
631
|
btn.innerHTML = icon;
|
|
481
632
|
return btn;
|
|
482
633
|
}
|
|
483
634
|
|
|
635
|
+
const toggleTreeBtn = actionBtn(ICON_PANEL_LEFT, t("workspace.toggleTree"));
|
|
636
|
+
toggleTreeBtn.classList.add("wv-toggle-tree", "active");
|
|
637
|
+
toggleTreeBtn.addEventListener("click", () => {
|
|
638
|
+
const hidden = onToggleTree ? onToggleTree() : false;
|
|
639
|
+
toggleTreeBtn.classList.toggle("active", !hidden);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const mobileBackBtn = actionBtn(ICON_ARROW_LEFT, t("workspace.backToFiles"), "wv-mobile-back");
|
|
643
|
+
mobileBackBtn.addEventListener("click", () => {
|
|
644
|
+
if (onShowMobileTree) onShowMobileTree();
|
|
645
|
+
});
|
|
646
|
+
|
|
484
647
|
const previewBtn = actionBtn(ICON_EYE, t("workspace.preview"));
|
|
648
|
+
const splitBtn = actionBtn(ICON_COLUMNS, t("workspace.splitPreview"));
|
|
485
649
|
const copyBtn = actionBtn(ICON_COPY, t("workspace.copyContent"));
|
|
486
|
-
const downloadBtn = actionBtn(ICON_DOWNLOAD, t("workspace.download"));
|
|
487
650
|
const saveBtn = actionBtn(ICON_SAVE, t("modal.save"));
|
|
651
|
+
|
|
652
|
+
// Split button shown for binary (non-previewable) tabs: text part opens
|
|
653
|
+
// with the OS default app, the chevron opens the "open with" dropdown.
|
|
654
|
+
const openGroup = el("div", "wv-action-group");
|
|
655
|
+
const openMainBtn = el("button", "wv-action wv-action--open");
|
|
656
|
+
openMainBtn.type = "button";
|
|
657
|
+
openMainBtn.title = t("workspace.openDefault");
|
|
658
|
+
const openIconEl = el("span", "wv-main-app-icon");
|
|
659
|
+
openIconEl.innerHTML = ICON_EXTERNAL;
|
|
660
|
+
const openLabel = el("span");
|
|
661
|
+
openLabel.textContent = t("workspace.open");
|
|
662
|
+
openMainBtn.appendChild(openIconEl);
|
|
663
|
+
openMainBtn.appendChild(openLabel);
|
|
664
|
+
openMainBtn.addEventListener("click", async () => {
|
|
665
|
+
const tab = activeTab();
|
|
666
|
+
if (!tab) return;
|
|
667
|
+
await openWithSystem(tab.entry);
|
|
668
|
+
});
|
|
669
|
+
const openArrowBtn = actionBtn(ICON_CHEVRON_DOWN, t("workspace.openWith"));
|
|
670
|
+
openArrowBtn.addEventListener("click", (e) => {
|
|
671
|
+
e.stopPropagation();
|
|
672
|
+
const tab = activeTab();
|
|
673
|
+
if (!tab) return;
|
|
674
|
+
showOpenWithMenu(openArrowBtn, tab.entry);
|
|
675
|
+
});
|
|
676
|
+
openGroup.appendChild(openMainBtn);
|
|
677
|
+
openGroup.appendChild(openArrowBtn);
|
|
678
|
+
|
|
679
|
+
actionsEl.appendChild(toggleTreeBtn);
|
|
488
680
|
actionsEl.appendChild(previewBtn);
|
|
681
|
+
actionsEl.appendChild(splitBtn);
|
|
489
682
|
actionsEl.appendChild(copyBtn);
|
|
490
|
-
actionsEl.appendChild(
|
|
683
|
+
actionsEl.appendChild(openGroup);
|
|
491
684
|
actionsEl.appendChild(saveBtn);
|
|
492
685
|
|
|
686
|
+
tabbar.appendChild(mobileBackBtn);
|
|
493
687
|
tabbar.appendChild(tabsEl);
|
|
494
688
|
tabbar.appendChild(actionsEl);
|
|
495
689
|
|
|
@@ -498,6 +692,19 @@ const WorkspaceView = (() => {
|
|
|
498
692
|
|
|
499
693
|
const activeTab = () => tabs.find((x) => x.id === activeId) || null;
|
|
500
694
|
|
|
695
|
+
// Hand a file to the OS default application. Returns whether it worked so
|
|
696
|
+
// callers can decide what to tell the user.
|
|
697
|
+
async function openWithSystem(entry) {
|
|
698
|
+
try {
|
|
699
|
+
await Workspace.openWithDefault(entry);
|
|
700
|
+
return true;
|
|
701
|
+
} catch (err) {
|
|
702
|
+
console.error("open failed:", err);
|
|
703
|
+
Modal.toast(t("workspace.openWithFailed") + ": " + err.message, "error");
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
501
708
|
function rebuildTabbar() {
|
|
502
709
|
tabsEl.replaceChildren();
|
|
503
710
|
tabs.forEach((tab) => {
|
|
@@ -538,12 +745,39 @@ const WorkspaceView = (() => {
|
|
|
538
745
|
|
|
539
746
|
function updateActions() {
|
|
540
747
|
const tab = activeTab();
|
|
748
|
+
openGroup.style.display = tab ? "" : "none";
|
|
541
749
|
previewBtn.style.display = tab && tab.kind === "text" && tab.isMd ? "" : "none";
|
|
750
|
+
splitBtn.style.display = tab && tab.kind === "text" && tab.isMd ? "" : "none";
|
|
542
751
|
copyBtn.style.display = tab && tab.kind === "text" ? "" : "none";
|
|
543
752
|
saveBtn.style.display = tab && tab.kind === "text" ? "" : "none";
|
|
544
|
-
downloadBtn.style.display = tab ? "" : "none";
|
|
545
753
|
previewBtn.classList.toggle("active", !!(tab && tab.previewMode));
|
|
754
|
+
splitBtn.classList.toggle("active", !!(tab && tab.splitMode));
|
|
546
755
|
saveBtn.classList.toggle("dirty", !!(tab && tab.dirty));
|
|
756
|
+
refreshOpenIcon();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Swap the main "Open" button icon to the default app's logo (asynchronous;
|
|
760
|
+
// falls back to the generic external-link icon). Guarded by tab id so a
|
|
761
|
+
// slow response never clobbers a newer tab.
|
|
762
|
+
let _openIconTabId = null;
|
|
763
|
+
function refreshOpenIcon() {
|
|
764
|
+
const tab = activeTab();
|
|
765
|
+
if (tab && tab.id === _openIconTabId) return;
|
|
766
|
+
_openIconTabId = tab ? tab.id : null;
|
|
767
|
+
openIconEl.innerHTML = ICON_EXTERNAL;
|
|
768
|
+
openMainBtn.title = t("workspace.openDefault");
|
|
769
|
+
if (!tab) return;
|
|
770
|
+
Workspace.fetchDefaultApp(tab.entry).then((app) => {
|
|
771
|
+
if (!app || _openIconTabId !== tab.id) return;
|
|
772
|
+
const img = document.createElement("img");
|
|
773
|
+
img.alt = "";
|
|
774
|
+
img.src = "/api/app-icon?path=" + encodeURIComponent(app.path);
|
|
775
|
+
img.addEventListener("error", () => {
|
|
776
|
+
if (_openIconTabId === tab.id) openIconEl.innerHTML = ICON_EXTERNAL;
|
|
777
|
+
});
|
|
778
|
+
openIconEl.replaceChildren(img);
|
|
779
|
+
openMainBtn.title = t("workspace.openWith", { app: app.name });
|
|
780
|
+
}).catch(() => { /* keep generic icon */ });
|
|
547
781
|
}
|
|
548
782
|
|
|
549
783
|
function activate(id) {
|
|
@@ -579,6 +813,7 @@ const WorkspaceView = (() => {
|
|
|
579
813
|
} else {
|
|
580
814
|
rebuildTabbar();
|
|
581
815
|
}
|
|
816
|
+
if (tabs.length === 0) restoreAsideWidth();
|
|
582
817
|
notify();
|
|
583
818
|
}
|
|
584
819
|
|
|
@@ -600,12 +835,24 @@ const WorkspaceView = (() => {
|
|
|
600
835
|
}
|
|
601
836
|
}
|
|
602
837
|
|
|
603
|
-
function
|
|
604
|
-
tab.
|
|
605
|
-
tab.
|
|
606
|
-
if (tab.previewMode && tab.editor) {
|
|
838
|
+
function applyMode(tab) {
|
|
839
|
+
tab.el.dataset.mode = tab.splitMode ? "split" : (tab.previewMode ? "preview" : "edit");
|
|
840
|
+
if ((tab.previewMode || tab.splitMode) && tab.editor) {
|
|
607
841
|
tab.previewEl.innerHTML = CodeEditor.renderMarkdown(tab.editor.getContent());
|
|
608
842
|
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function togglePreview(tab) {
|
|
846
|
+
tab.previewMode = !tab.previewMode;
|
|
847
|
+
tab.splitMode = false;
|
|
848
|
+
applyMode(tab);
|
|
849
|
+
updateActions();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function toggleSplit(tab) {
|
|
853
|
+
tab.splitMode = !tab.splitMode;
|
|
854
|
+
tab.previewMode = false;
|
|
855
|
+
applyMode(tab);
|
|
609
856
|
updateActions();
|
|
610
857
|
}
|
|
611
858
|
|
|
@@ -627,6 +874,7 @@ const WorkspaceView = (() => {
|
|
|
627
874
|
dirty: false,
|
|
628
875
|
isMd,
|
|
629
876
|
previewMode: false,
|
|
877
|
+
splitMode: false,
|
|
630
878
|
previewEl: null,
|
|
631
879
|
editor: null,
|
|
632
880
|
imageUrl: null,
|
|
@@ -646,6 +894,9 @@ const WorkspaceView = (() => {
|
|
|
646
894
|
tab.dirty = true;
|
|
647
895
|
syncDirty(tab);
|
|
648
896
|
}
|
|
897
|
+
if (tab.splitMode && tab.previewEl) {
|
|
898
|
+
tab.previewEl.innerHTML = CodeEditor.renderMarkdown(tab.editor.getContent());
|
|
899
|
+
}
|
|
649
900
|
},
|
|
650
901
|
onSave: () => doSave(tab),
|
|
651
902
|
});
|
|
@@ -673,42 +924,147 @@ const WorkspaceView = (() => {
|
|
|
673
924
|
};
|
|
674
925
|
}
|
|
675
926
|
|
|
927
|
+
function buildPdfTab(entry, url) {
|
|
928
|
+
const pane = el("div", "wv-pane wv-pane--pdf");
|
|
929
|
+
const frame = document.createElement("iframe");
|
|
930
|
+
frame.title = entry.name;
|
|
931
|
+
frame.src = url;
|
|
932
|
+
pane.appendChild(frame);
|
|
933
|
+
return {
|
|
934
|
+
id: entry.path,
|
|
935
|
+
entry,
|
|
936
|
+
kind: "pdf",
|
|
937
|
+
el: pane,
|
|
938
|
+
dirty: false,
|
|
939
|
+
isMd: false,
|
|
940
|
+
previewMode: false,
|
|
941
|
+
previewEl: null,
|
|
942
|
+
editor: null,
|
|
943
|
+
imageUrl: null,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async function buildFallbackTab(entry) {
|
|
948
|
+
const pane = el("div", "wv-pane wv-pane--fallback");
|
|
949
|
+
|
|
950
|
+
const icon = el("div", "wv-fallback-icon");
|
|
951
|
+
icon.innerHTML = fileIconSvg(entry.name);
|
|
952
|
+
|
|
953
|
+
const name = el("div", "wv-fallback-name");
|
|
954
|
+
name.textContent = entry.name;
|
|
955
|
+
|
|
956
|
+
const hint = el("div", "wv-fallback-hint");
|
|
957
|
+
hint.textContent = t("workspace.fallbackHint");
|
|
958
|
+
|
|
959
|
+
const subhint = el("div", "wv-fallback-subhint");
|
|
960
|
+
subhint.textContent = t("workspace.fallbackSubHint");
|
|
961
|
+
|
|
962
|
+
pane.appendChild(icon);
|
|
963
|
+
pane.appendChild(name);
|
|
964
|
+
pane.appendChild(hint);
|
|
965
|
+
pane.appendChild(subhint);
|
|
966
|
+
|
|
967
|
+
return {
|
|
968
|
+
id: entry.path,
|
|
969
|
+
entry,
|
|
970
|
+
kind: "binary",
|
|
971
|
+
el: pane,
|
|
972
|
+
subhintEl: subhint,
|
|
973
|
+
dirty: false,
|
|
974
|
+
isMd: false,
|
|
975
|
+
previewMode: false,
|
|
976
|
+
previewEl: null,
|
|
977
|
+
editor: null,
|
|
978
|
+
imageUrl: null,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function buildTableTab(entry, text) {
|
|
983
|
+
const pane = el("div", "wv-pane wv-pane--table");
|
|
984
|
+
const scroller = el("div", "wv-table-scroll");
|
|
985
|
+
|
|
986
|
+
const result = CodeEditor.renderTable(text, entry.name);
|
|
987
|
+
scroller.appendChild(result.table);
|
|
988
|
+
pane.appendChild(scroller);
|
|
989
|
+
|
|
990
|
+
if (result.truncated) {
|
|
991
|
+
const note = el("div", "wv-table-note");
|
|
992
|
+
note.textContent = t("workspace.tableTruncated").replace("%{rows}", result.rows);
|
|
993
|
+
pane.appendChild(note);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return {
|
|
997
|
+
id: entry.path,
|
|
998
|
+
entry,
|
|
999
|
+
kind: "table",
|
|
1000
|
+
el: pane,
|
|
1001
|
+
dirty: false,
|
|
1002
|
+
isMd: false,
|
|
1003
|
+
previewMode: false,
|
|
1004
|
+
splitMode: false,
|
|
1005
|
+
previewEl: null,
|
|
1006
|
+
editor: null,
|
|
1007
|
+
imageUrl: null,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
676
1011
|
async function openTab(entry) {
|
|
677
|
-
ensureAsideWidth();
|
|
678
1012
|
const existing = tabs.find((x) => x.id === entry.path);
|
|
679
1013
|
if (existing) {
|
|
680
1014
|
activate(existing.id);
|
|
681
1015
|
return;
|
|
682
1016
|
}
|
|
683
1017
|
const kind = CodeEditor.fileKind(entry.name);
|
|
684
|
-
if (kind === "binary") {
|
|
685
|
-
Modal.toast(t("workspace.previewUnsupported"), "info");
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
1018
|
let tab = null;
|
|
1019
|
+
let handOff = false;
|
|
689
1020
|
try {
|
|
690
1021
|
if (kind === "image") {
|
|
691
1022
|
const blob = await Workspace.fetchFileBlob(entry);
|
|
692
1023
|
tab = buildImageTab(entry, URL.createObjectURL(blob));
|
|
1024
|
+
} else if (kind === "pdf") {
|
|
1025
|
+
tab = buildPdfTab(entry, "/api/local-image?path=" + encodeURIComponent(Workspace.absPath(entry.path)));
|
|
1026
|
+
} else if (kind === "binary") {
|
|
1027
|
+
// The extension already says the browser cannot render this, so the
|
|
1028
|
+
// OS application the user was going to pick is launched below without
|
|
1029
|
+
// making them click a second time. Files that only look binary once
|
|
1030
|
+
// their bytes are sniffed are left alone: that is a damaged or
|
|
1031
|
+
// mislabelled file, not a format the system has a viewer for.
|
|
1032
|
+
handOff = autoOpenWithSystem(entry.name);
|
|
1033
|
+
tab = await buildFallbackTab(entry);
|
|
1034
|
+
} else if (kind === "table") {
|
|
1035
|
+
const text = await Workspace.fetchFileText(entry);
|
|
1036
|
+
tab = CodeEditor.looksBinary(text) ? await buildFallbackTab(entry) : buildTableTab(entry, text);
|
|
693
1037
|
} else {
|
|
694
1038
|
const text = await Workspace.fetchFileText(entry);
|
|
695
|
-
tab = buildTextTab(entry, text);
|
|
1039
|
+
tab = CodeEditor.looksBinary(text) ? await buildFallbackTab(entry) : buildTextTab(entry, text);
|
|
696
1040
|
}
|
|
697
1041
|
} catch (err) {
|
|
698
1042
|
console.error("preview failed:", err);
|
|
699
|
-
Modal.toast(t("workspace.previewFailed"), "error");
|
|
1043
|
+
Modal.toast(t("workspace.previewFailed") + ": " + err.message, "error");
|
|
700
1044
|
return;
|
|
701
1045
|
}
|
|
1046
|
+
if (tabs.length === 0) ensureAsideWidth();
|
|
702
1047
|
tabs.push(tab);
|
|
703
1048
|
content.appendChild(tab.el);
|
|
704
1049
|
activate(tab.id);
|
|
705
1050
|
notify();
|
|
1051
|
+
|
|
1052
|
+
// Nothing to show in the pane, so skip the extra click and launch the
|
|
1053
|
+
// application the user was going to pick anyway.
|
|
1054
|
+
if (handOff) {
|
|
1055
|
+
const opened = await openWithSystem(tab.entry);
|
|
1056
|
+
if (opened && tab.subhintEl) tab.subhintEl.textContent = t("workspace.openedInSystem");
|
|
1057
|
+
}
|
|
706
1058
|
}
|
|
707
1059
|
|
|
708
1060
|
previewBtn.addEventListener("click", () => {
|
|
709
1061
|
const tab = activeTab();
|
|
710
1062
|
if (tab && tab.isMd) togglePreview(tab);
|
|
711
1063
|
});
|
|
1064
|
+
splitBtn.addEventListener("click", () => {
|
|
1065
|
+
const tab = activeTab();
|
|
1066
|
+
if (tab && tab.isMd) toggleSplit(tab);
|
|
1067
|
+
});
|
|
712
1068
|
copyBtn.addEventListener("click", async () => {
|
|
713
1069
|
const tab = activeTab();
|
|
714
1070
|
if (!tab || !tab.editor) return;
|
|
@@ -719,16 +1075,13 @@ const WorkspaceView = (() => {
|
|
|
719
1075
|
Modal.toast(t("workspace.copyFailed"), "error");
|
|
720
1076
|
}
|
|
721
1077
|
});
|
|
722
|
-
downloadBtn.addEventListener("click", () => {
|
|
723
|
-
const tab = activeTab();
|
|
724
|
-
if (tab) downloadFile(tab.entry);
|
|
725
|
-
});
|
|
726
1078
|
saveBtn.addEventListener("click", () => {
|
|
727
1079
|
const tab = activeTab();
|
|
728
1080
|
if (tab) doSave(tab);
|
|
729
1081
|
});
|
|
730
1082
|
|
|
731
1083
|
function destroy() {
|
|
1084
|
+
restoreAsideWidth();
|
|
732
1085
|
tabs.forEach((tab) => {
|
|
733
1086
|
if (tab.editor) tab.editor.destroy();
|
|
734
1087
|
if (tab.imageUrl) URL.revokeObjectURL(tab.imageUrl);
|
|
@@ -767,6 +1120,7 @@ const WorkspaceView = (() => {
|
|
|
767
1120
|
const onUp = () => {
|
|
768
1121
|
if (!dragging) return;
|
|
769
1122
|
dragging = false;
|
|
1123
|
+
handle.classList.remove("active");
|
|
770
1124
|
document.body.style.cursor = "";
|
|
771
1125
|
document.body.style.userSelect = "";
|
|
772
1126
|
document.removeEventListener("mousemove", onMove);
|
|
@@ -780,6 +1134,7 @@ const WorkspaceView = (() => {
|
|
|
780
1134
|
dragging = true;
|
|
781
1135
|
startX = e.clientX;
|
|
782
1136
|
startW = treePane.getBoundingClientRect().width;
|
|
1137
|
+
handle.classList.add("active");
|
|
783
1138
|
document.body.style.cursor = "col-resize";
|
|
784
1139
|
document.body.style.userSelect = "none";
|
|
785
1140
|
document.addEventListener("mousemove", onMove);
|
|
@@ -833,10 +1188,22 @@ const WorkspaceView = (() => {
|
|
|
833
1188
|
});
|
|
834
1189
|
tree.addEventListener("mouseleave", () => updateIndentGuides(null));
|
|
835
1190
|
|
|
836
|
-
const viewer = createViewer(
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1191
|
+
const viewer = createViewer(
|
|
1192
|
+
({ count, activeId }) => {
|
|
1193
|
+
root.classList.toggle("has-open-files", count > 0);
|
|
1194
|
+
if (activeId) root.classList.remove("mobile-tree-visible");
|
|
1195
|
+
if (!count) root.classList.remove("mobile-tree-visible");
|
|
1196
|
+
setActiveFilePath(activeId);
|
|
1197
|
+
},
|
|
1198
|
+
() => {
|
|
1199
|
+
const hidden = root.classList.toggle("tree-hidden");
|
|
1200
|
+
return hidden;
|
|
1201
|
+
},
|
|
1202
|
+
() => {
|
|
1203
|
+
root.classList.add("mobile-tree-visible");
|
|
1204
|
+
}
|
|
1205
|
+
);
|
|
1206
|
+
_activeViewer = viewer;
|
|
840
1207
|
|
|
841
1208
|
// ── File search ─────────────────────────────────────────────────────
|
|
842
1209
|
|
|
@@ -951,10 +1318,9 @@ const WorkspaceView = (() => {
|
|
|
951
1318
|
});
|
|
952
1319
|
|
|
953
1320
|
refresh.addEventListener("click", () => {
|
|
954
|
-
treeState.indexPromise = null;
|
|
955
1321
|
searchInput.value = "";
|
|
956
1322
|
showTree();
|
|
957
|
-
|
|
1323
|
+
reloadTree();
|
|
958
1324
|
});
|
|
959
1325
|
|
|
960
1326
|
treePane.appendChild(bar);
|
|
@@ -969,22 +1335,87 @@ const WorkspaceView = (() => {
|
|
|
969
1335
|
root.appendChild(viewer.root);
|
|
970
1336
|
container.appendChild(root);
|
|
971
1337
|
|
|
972
|
-
|
|
1338
|
+
// One entry point for (re)loading the root listing. openFile() waits on the
|
|
1339
|
+
// same promise before revealing a path, so a link clicked while the tree is
|
|
1340
|
+
// still loading does not miss its row.
|
|
1341
|
+
function reloadTree() {
|
|
1342
|
+
treeState.indexPromise = null;
|
|
1343
|
+
treeState.loadPromise = loadRoot(tree, viewer.openTab);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
reloadTree();
|
|
1347
|
+
|
|
1348
|
+
// The tree is otherwise loaded only once per mount. Reload it when the
|
|
1349
|
+
// working directory changes (status-bar dir switch) and when the agent
|
|
1350
|
+
// finishes a task (it may have created/edited files under the dir).
|
|
1351
|
+
if (_activeOff) _activeOff();
|
|
1352
|
+
const offSessionChanged = Workspace.on("workspace:sessionChanged", () => {
|
|
1353
|
+
reloadTree();
|
|
1354
|
+
});
|
|
1355
|
+
const offTaskComplete = Workspace.on("workspace:taskCompleted", (payload) => {
|
|
1356
|
+
if (payload && payload.sessionId === Workspace.state.sessionId) {
|
|
1357
|
+
reloadTree();
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
const offAll = () => { offSessionChanged(); offTaskComplete(); };
|
|
1361
|
+
_activeOff = offAll;
|
|
1362
|
+
|
|
973
1363
|
return {
|
|
974
1364
|
destroy() {
|
|
1365
|
+
if (_activeOff === offAll) {
|
|
1366
|
+
offAll();
|
|
1367
|
+
_activeOff = null;
|
|
1368
|
+
}
|
|
975
1369
|
clearTimeout(searchTimer);
|
|
976
1370
|
treeState.treeEl = null;
|
|
977
1371
|
treeState.activePath = null;
|
|
978
1372
|
treeState.indexPromise = null;
|
|
1373
|
+
treeState.loadPromise = null;
|
|
979
1374
|
lastGuidedRow = null;
|
|
1375
|
+
if (_activeViewer === viewer) _activeViewer = null;
|
|
980
1376
|
viewer.destroy();
|
|
981
1377
|
},
|
|
982
1378
|
};
|
|
983
1379
|
}
|
|
984
1380
|
|
|
985
|
-
|
|
1381
|
+
// Convert an absolute path (from a file:// link in chat) into a tree-style
|
|
1382
|
+
// entry. Paths inside the working directory become relative so tab
|
|
1383
|
+
// de-duplication matches tabs opened from the tree; paths outside stay
|
|
1384
|
+
// absolute (the store treats absolute paths as-is).
|
|
1385
|
+
function toEntry(absPath) {
|
|
1386
|
+
const wd = (Workspace.state.workingDir || "").replace(/\/+$/, "");
|
|
1387
|
+
const name = absPath.split("/").pop() || absPath;
|
|
1388
|
+
if (wd && absPath.startsWith(wd + "/")) {
|
|
1389
|
+
return { name, path: absPath.slice(wd.length + 1), type: "file", size: 0 };
|
|
1390
|
+
}
|
|
1391
|
+
return { name, path: absPath, type: "file", size: 0 };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Open a file:// link target in the Files tab of the session aside.
|
|
1395
|
+
// Returns true when the viewer handled it; false lets the caller fall back
|
|
1396
|
+
// to the system open action. HTML files open in the preview panel instead
|
|
1397
|
+
// (rendered as a page, not shown as source).
|
|
1398
|
+
function openFile(absPath) {
|
|
1399
|
+
if (!absPath || !Workspace.state.hasSession()) return false;
|
|
1400
|
+
const entry = toEntry(absPath);
|
|
1401
|
+
if (/\.html?$/i.test(entry.name) && window.Clacky && Clacky.Preview) {
|
|
1402
|
+
return Clacky.Preview.open(entry.path);
|
|
1403
|
+
}
|
|
1404
|
+
if (window.Clacky && Clacky.Aside) Clacky.Aside.open();
|
|
1405
|
+
const tabBtn = document.querySelector('.aside-tab[data-tab="files"]');
|
|
1406
|
+
if (tabBtn) tabBtn.click();
|
|
1407
|
+
if (!_activeViewer) return false;
|
|
1408
|
+
_activeViewer.openTab(entry);
|
|
1409
|
+
revealTreePath(entry.path);
|
|
1410
|
+
return true;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// autoOpenWithSystem is exported for the regression harness.
|
|
1414
|
+
return { mount, openFile, autoOpenWithSystem };
|
|
986
1415
|
})();
|
|
987
1416
|
|
|
1417
|
+
if (window.Clacky) Clacky.WorkspaceView = WorkspaceView;
|
|
1418
|
+
|
|
988
1419
|
// Files is a built-in tab: visible for every session, after ext-studio
|
|
989
1420
|
// publish (20) and before git (30) / time-machine (40).
|
|
990
1421
|
if (window.Clacky && Clacky.ext) {
|