openclacky 1.5.13 → 1.5.14
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 +51 -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 +38 -97
- data/lib/clacky/agent.rb +281 -276
- data/lib/clacky/agent_config.rb +1 -1
- data/lib/clacky/billing/billing_store.rb +2 -2
- data/lib/clacky/billing/platform_billing.rb +6 -0
- data/lib/clacky/brand_config.rb +13 -2
- data/lib/clacky/cli.rb +27 -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/panels/studio/view.js +91 -5
- 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 +1 -1
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/openai_compat.rb +70 -30
- data/lib/clacky/message_format/bedrock.rb +6 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/providers.rb +38 -3
- data/lib/clacky/rich_ui/rich_ui_controller.rb +1 -1
- data/lib/clacky/search_config.rb +3 -3
- data/lib/clacky/server/channel/channel_manager.rb +8 -5
- data/lib/clacky/server/git_panel.rb +10 -2
- data/lib/clacky/server/http_server.rb +161 -14
- data/lib/clacky/server/preview.rb +351 -0
- data/lib/clacky/server/web_ui_controller.rb +1 -1
- data/lib/clacky/skill.rb +48 -0
- data/lib/clacky/tools/browser.rb +176 -19
- data/lib/clacky/tools/web_search.rb +79 -8
- data/lib/clacky/ui2/components/command_suggestions.rb +2 -1
- data/lib/clacky/ui2/components/input_area.rb +22 -2
- data/lib/clacky/ui2/components/modal_component.rb +35 -6
- data/lib/clacky/ui2/ui_controller.rb +44 -7
- 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 +114 -66
- data/lib/clacky/utils/windows_app_detector.rb +334 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +596 -130
- 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/model-picker.js +2 -1
- data/lib/clacky/web/components/quote-select.js +235 -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/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 +392 -40
- data/lib/clacky/web/i18n.js +90 -5
- data/lib/clacky/web/index.html +15 -6
- data/lib/clacky/web/sessions.js +193 -38
- 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 +20 -0
- data/lib/clacky.rb +2 -0
- metadata +11 -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);
|
|
@@ -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;
|
|
@@ -443,27 +536,36 @@ const WorkspaceView = (() => {
|
|
|
443
536
|
|
|
444
537
|
// ── Inline tabbed viewer ────────────────────────────────────────────────
|
|
445
538
|
|
|
446
|
-
|
|
447
|
-
// the first file is opened (still user-resizable afterwards).
|
|
448
|
-
function ensureAsideWidth() {
|
|
449
|
-
const aside = document.getElementById("session-aside");
|
|
450
|
-
if (!aside) return;
|
|
451
|
-
let w = 0;
|
|
452
|
-
try {
|
|
453
|
-
w = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width")) || 0;
|
|
454
|
-
} catch (_e) { w = 0; }
|
|
455
|
-
const target = 620;
|
|
456
|
-
if (w < target) {
|
|
457
|
-
aside.style.setProperty("--session-aside-width", target + "px");
|
|
458
|
-
try { localStorage.setItem("clacky.aside.width", String(target)); } catch (_e) { /* non-fatal */ }
|
|
459
|
-
}
|
|
460
|
-
}
|
|
539
|
+
const VIEWER_ASIDE_WIDTH = 700;
|
|
461
540
|
|
|
462
|
-
|
|
541
|
+
// Active viewer instance (one per mounted Files tab). Kept at module level
|
|
542
|
+
// so openFile() — called from chat file:// link clicks — can reach the
|
|
543
|
+
// viewer without waiting for a DOM query or a custom event round-trip.
|
|
544
|
+
let _activeViewer = null;
|
|
545
|
+
// Unsubscriber for the active mount's refresh listeners. The aside can
|
|
546
|
+
// re-mount the Files tab without running the previous teardown (new-session
|
|
547
|
+
// page / same-session re-render), so the next mount releases these first to
|
|
548
|
+
// avoid stacking duplicate reloads.
|
|
549
|
+
let _activeOff = null;
|
|
550
|
+
|
|
551
|
+
function createViewer(onTabsChanged, onToggleTree, onShowMobileTree) {
|
|
463
552
|
const tabs = [];
|
|
464
553
|
let activeId = null;
|
|
554
|
+
let releaseAsideWidth = null;
|
|
465
555
|
const notify = () => { if (onTabsChanged) onTabsChanged({ count: tabs.length, activeId }); };
|
|
466
556
|
|
|
557
|
+
function ensureAsideWidth() {
|
|
558
|
+
if (releaseAsideWidth || !window.Clacky || !Clacky.Aside || !Clacky.Aside.requestWidth) return;
|
|
559
|
+
releaseAsideWidth = Clacky.Aside.requestWidth(VIEWER_ASIDE_WIDTH);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function restoreAsideWidth() {
|
|
563
|
+
if (!releaseAsideWidth) return;
|
|
564
|
+
const release = releaseAsideWidth;
|
|
565
|
+
releaseAsideWidth = null;
|
|
566
|
+
release();
|
|
567
|
+
}
|
|
568
|
+
|
|
467
569
|
const root = el("div", "wv-viewer");
|
|
468
570
|
|
|
469
571
|
const tabbar = el("div", "wv-tabbar");
|
|
@@ -477,19 +579,68 @@ const WorkspaceView = (() => {
|
|
|
477
579
|
const btn = el("button", "wv-action" + (cls ? " " + cls : ""));
|
|
478
580
|
btn.type = "button";
|
|
479
581
|
btn.title = title;
|
|
582
|
+
btn.setAttribute("aria-label", title);
|
|
480
583
|
btn.innerHTML = icon;
|
|
481
584
|
return btn;
|
|
482
585
|
}
|
|
483
586
|
|
|
587
|
+
const toggleTreeBtn = actionBtn(ICON_PANEL_LEFT, t("workspace.toggleTree"));
|
|
588
|
+
toggleTreeBtn.classList.add("wv-toggle-tree", "active");
|
|
589
|
+
toggleTreeBtn.addEventListener("click", () => {
|
|
590
|
+
const hidden = onToggleTree ? onToggleTree() : false;
|
|
591
|
+
toggleTreeBtn.classList.toggle("active", !hidden);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const mobileBackBtn = actionBtn(ICON_ARROW_LEFT, t("workspace.backToFiles"), "wv-mobile-back");
|
|
595
|
+
mobileBackBtn.addEventListener("click", () => {
|
|
596
|
+
if (onShowMobileTree) onShowMobileTree();
|
|
597
|
+
});
|
|
598
|
+
|
|
484
599
|
const previewBtn = actionBtn(ICON_EYE, t("workspace.preview"));
|
|
600
|
+
const splitBtn = actionBtn(ICON_COLUMNS, t("workspace.splitPreview"));
|
|
485
601
|
const copyBtn = actionBtn(ICON_COPY, t("workspace.copyContent"));
|
|
486
|
-
const downloadBtn = actionBtn(ICON_DOWNLOAD, t("workspace.download"));
|
|
487
602
|
const saveBtn = actionBtn(ICON_SAVE, t("modal.save"));
|
|
603
|
+
|
|
604
|
+
// Split button shown for binary (non-previewable) tabs: text part opens
|
|
605
|
+
// with the OS default app, the chevron opens the "open with" dropdown.
|
|
606
|
+
const openGroup = el("div", "wv-action-group");
|
|
607
|
+
const openMainBtn = el("button", "wv-action wv-action--open");
|
|
608
|
+
openMainBtn.type = "button";
|
|
609
|
+
openMainBtn.title = t("workspace.openDefault");
|
|
610
|
+
const openIconEl = el("span", "wv-main-app-icon");
|
|
611
|
+
openIconEl.innerHTML = ICON_EXTERNAL;
|
|
612
|
+
const openLabel = el("span");
|
|
613
|
+
openLabel.textContent = t("workspace.open");
|
|
614
|
+
openMainBtn.appendChild(openIconEl);
|
|
615
|
+
openMainBtn.appendChild(openLabel);
|
|
616
|
+
openMainBtn.addEventListener("click", async () => {
|
|
617
|
+
const tab = activeTab();
|
|
618
|
+
if (!tab) return;
|
|
619
|
+
try {
|
|
620
|
+
await Workspace.openWithDefault(tab.entry);
|
|
621
|
+
} catch (err) {
|
|
622
|
+
console.error("open failed:", err);
|
|
623
|
+
Modal.toast(t("workspace.openWithFailed") + ": " + err.message, "error");
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
const openArrowBtn = actionBtn(ICON_CHEVRON_DOWN, t("workspace.openWith"));
|
|
627
|
+
openArrowBtn.addEventListener("click", (e) => {
|
|
628
|
+
e.stopPropagation();
|
|
629
|
+
const tab = activeTab();
|
|
630
|
+
if (!tab) return;
|
|
631
|
+
showOpenWithMenu(openArrowBtn, tab.entry);
|
|
632
|
+
});
|
|
633
|
+
openGroup.appendChild(openMainBtn);
|
|
634
|
+
openGroup.appendChild(openArrowBtn);
|
|
635
|
+
|
|
636
|
+
actionsEl.appendChild(toggleTreeBtn);
|
|
488
637
|
actionsEl.appendChild(previewBtn);
|
|
638
|
+
actionsEl.appendChild(splitBtn);
|
|
489
639
|
actionsEl.appendChild(copyBtn);
|
|
490
|
-
actionsEl.appendChild(
|
|
640
|
+
actionsEl.appendChild(openGroup);
|
|
491
641
|
actionsEl.appendChild(saveBtn);
|
|
492
642
|
|
|
643
|
+
tabbar.appendChild(mobileBackBtn);
|
|
493
644
|
tabbar.appendChild(tabsEl);
|
|
494
645
|
tabbar.appendChild(actionsEl);
|
|
495
646
|
|
|
@@ -538,12 +689,39 @@ const WorkspaceView = (() => {
|
|
|
538
689
|
|
|
539
690
|
function updateActions() {
|
|
540
691
|
const tab = activeTab();
|
|
692
|
+
openGroup.style.display = tab ? "" : "none";
|
|
541
693
|
previewBtn.style.display = tab && tab.kind === "text" && tab.isMd ? "" : "none";
|
|
694
|
+
splitBtn.style.display = tab && tab.kind === "text" && tab.isMd ? "" : "none";
|
|
542
695
|
copyBtn.style.display = tab && tab.kind === "text" ? "" : "none";
|
|
543
696
|
saveBtn.style.display = tab && tab.kind === "text" ? "" : "none";
|
|
544
|
-
downloadBtn.style.display = tab ? "" : "none";
|
|
545
697
|
previewBtn.classList.toggle("active", !!(tab && tab.previewMode));
|
|
698
|
+
splitBtn.classList.toggle("active", !!(tab && tab.splitMode));
|
|
546
699
|
saveBtn.classList.toggle("dirty", !!(tab && tab.dirty));
|
|
700
|
+
refreshOpenIcon();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Swap the main "Open" button icon to the default app's logo (asynchronous;
|
|
704
|
+
// falls back to the generic external-link icon). Guarded by tab id so a
|
|
705
|
+
// slow response never clobbers a newer tab.
|
|
706
|
+
let _openIconTabId = null;
|
|
707
|
+
function refreshOpenIcon() {
|
|
708
|
+
const tab = activeTab();
|
|
709
|
+
if (tab && tab.id === _openIconTabId) return;
|
|
710
|
+
_openIconTabId = tab ? tab.id : null;
|
|
711
|
+
openIconEl.innerHTML = ICON_EXTERNAL;
|
|
712
|
+
openMainBtn.title = t("workspace.openDefault");
|
|
713
|
+
if (!tab) return;
|
|
714
|
+
Workspace.fetchDefaultApp(tab.entry).then((app) => {
|
|
715
|
+
if (!app || _openIconTabId !== tab.id) return;
|
|
716
|
+
const img = document.createElement("img");
|
|
717
|
+
img.alt = "";
|
|
718
|
+
img.src = "/api/app-icon?path=" + encodeURIComponent(app.path);
|
|
719
|
+
img.addEventListener("error", () => {
|
|
720
|
+
if (_openIconTabId === tab.id) openIconEl.innerHTML = ICON_EXTERNAL;
|
|
721
|
+
});
|
|
722
|
+
openIconEl.replaceChildren(img);
|
|
723
|
+
openMainBtn.title = t("workspace.openWith", { app: app.name });
|
|
724
|
+
}).catch(() => { /* keep generic icon */ });
|
|
547
725
|
}
|
|
548
726
|
|
|
549
727
|
function activate(id) {
|
|
@@ -579,6 +757,7 @@ const WorkspaceView = (() => {
|
|
|
579
757
|
} else {
|
|
580
758
|
rebuildTabbar();
|
|
581
759
|
}
|
|
760
|
+
if (tabs.length === 0) restoreAsideWidth();
|
|
582
761
|
notify();
|
|
583
762
|
}
|
|
584
763
|
|
|
@@ -600,12 +779,24 @@ const WorkspaceView = (() => {
|
|
|
600
779
|
}
|
|
601
780
|
}
|
|
602
781
|
|
|
603
|
-
function
|
|
604
|
-
tab.
|
|
605
|
-
tab.
|
|
606
|
-
if (tab.previewMode && tab.editor) {
|
|
782
|
+
function applyMode(tab) {
|
|
783
|
+
tab.el.dataset.mode = tab.splitMode ? "split" : (tab.previewMode ? "preview" : "edit");
|
|
784
|
+
if ((tab.previewMode || tab.splitMode) && tab.editor) {
|
|
607
785
|
tab.previewEl.innerHTML = CodeEditor.renderMarkdown(tab.editor.getContent());
|
|
608
786
|
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function togglePreview(tab) {
|
|
790
|
+
tab.previewMode = !tab.previewMode;
|
|
791
|
+
tab.splitMode = false;
|
|
792
|
+
applyMode(tab);
|
|
793
|
+
updateActions();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function toggleSplit(tab) {
|
|
797
|
+
tab.splitMode = !tab.splitMode;
|
|
798
|
+
tab.previewMode = false;
|
|
799
|
+
applyMode(tab);
|
|
609
800
|
updateActions();
|
|
610
801
|
}
|
|
611
802
|
|
|
@@ -627,6 +818,7 @@ const WorkspaceView = (() => {
|
|
|
627
818
|
dirty: false,
|
|
628
819
|
isMd,
|
|
629
820
|
previewMode: false,
|
|
821
|
+
splitMode: false,
|
|
630
822
|
previewEl: null,
|
|
631
823
|
editor: null,
|
|
632
824
|
imageUrl: null,
|
|
@@ -646,6 +838,9 @@ const WorkspaceView = (() => {
|
|
|
646
838
|
tab.dirty = true;
|
|
647
839
|
syncDirty(tab);
|
|
648
840
|
}
|
|
841
|
+
if (tab.splitMode && tab.previewEl) {
|
|
842
|
+
tab.previewEl.innerHTML = CodeEditor.renderMarkdown(tab.editor.getContent());
|
|
843
|
+
}
|
|
649
844
|
},
|
|
650
845
|
onSave: () => doSave(tab),
|
|
651
846
|
});
|
|
@@ -673,32 +868,118 @@ const WorkspaceView = (() => {
|
|
|
673
868
|
};
|
|
674
869
|
}
|
|
675
870
|
|
|
871
|
+
function buildPdfTab(entry, url) {
|
|
872
|
+
const pane = el("div", "wv-pane wv-pane--pdf");
|
|
873
|
+
const frame = document.createElement("iframe");
|
|
874
|
+
frame.title = entry.name;
|
|
875
|
+
frame.src = url;
|
|
876
|
+
pane.appendChild(frame);
|
|
877
|
+
return {
|
|
878
|
+
id: entry.path,
|
|
879
|
+
entry,
|
|
880
|
+
kind: "pdf",
|
|
881
|
+
el: pane,
|
|
882
|
+
dirty: false,
|
|
883
|
+
isMd: false,
|
|
884
|
+
previewMode: false,
|
|
885
|
+
previewEl: null,
|
|
886
|
+
editor: null,
|
|
887
|
+
imageUrl: null,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function buildFallbackTab(entry) {
|
|
892
|
+
const pane = el("div", "wv-pane wv-pane--fallback");
|
|
893
|
+
|
|
894
|
+
const icon = el("div", "wv-fallback-icon");
|
|
895
|
+
icon.innerHTML = fileIconSvg(entry.name);
|
|
896
|
+
|
|
897
|
+
const name = el("div", "wv-fallback-name");
|
|
898
|
+
name.textContent = entry.name;
|
|
899
|
+
|
|
900
|
+
const hint = el("div", "wv-fallback-hint");
|
|
901
|
+
hint.textContent = t("workspace.fallbackHint");
|
|
902
|
+
|
|
903
|
+
const subhint = el("div", "wv-fallback-subhint");
|
|
904
|
+
subhint.textContent = t("workspace.fallbackSubHint");
|
|
905
|
+
|
|
906
|
+
pane.appendChild(icon);
|
|
907
|
+
pane.appendChild(name);
|
|
908
|
+
pane.appendChild(hint);
|
|
909
|
+
pane.appendChild(subhint);
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
id: entry.path,
|
|
913
|
+
entry,
|
|
914
|
+
kind: "binary",
|
|
915
|
+
el: pane,
|
|
916
|
+
dirty: false,
|
|
917
|
+
isMd: false,
|
|
918
|
+
previewMode: false,
|
|
919
|
+
previewEl: null,
|
|
920
|
+
editor: null,
|
|
921
|
+
imageUrl: null,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function buildTableTab(entry, text) {
|
|
926
|
+
const pane = el("div", "wv-pane wv-pane--table");
|
|
927
|
+
const scroller = el("div", "wv-table-scroll");
|
|
928
|
+
|
|
929
|
+
const result = CodeEditor.renderTable(text, entry.name);
|
|
930
|
+
scroller.appendChild(result.table);
|
|
931
|
+
pane.appendChild(scroller);
|
|
932
|
+
|
|
933
|
+
if (result.truncated) {
|
|
934
|
+
const note = el("div", "wv-table-note");
|
|
935
|
+
note.textContent = t("workspace.tableTruncated").replace("%{rows}", result.rows);
|
|
936
|
+
pane.appendChild(note);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
id: entry.path,
|
|
941
|
+
entry,
|
|
942
|
+
kind: "table",
|
|
943
|
+
el: pane,
|
|
944
|
+
dirty: false,
|
|
945
|
+
isMd: false,
|
|
946
|
+
previewMode: false,
|
|
947
|
+
splitMode: false,
|
|
948
|
+
previewEl: null,
|
|
949
|
+
editor: null,
|
|
950
|
+
imageUrl: null,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
676
954
|
async function openTab(entry) {
|
|
677
|
-
ensureAsideWidth();
|
|
678
955
|
const existing = tabs.find((x) => x.id === entry.path);
|
|
679
956
|
if (existing) {
|
|
680
957
|
activate(existing.id);
|
|
681
958
|
return;
|
|
682
959
|
}
|
|
683
960
|
const kind = CodeEditor.fileKind(entry.name);
|
|
684
|
-
if (kind === "binary") {
|
|
685
|
-
Modal.toast(t("workspace.previewUnsupported"), "info");
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
961
|
let tab = null;
|
|
689
962
|
try {
|
|
690
963
|
if (kind === "image") {
|
|
691
964
|
const blob = await Workspace.fetchFileBlob(entry);
|
|
692
965
|
tab = buildImageTab(entry, URL.createObjectURL(blob));
|
|
966
|
+
} else if (kind === "pdf") {
|
|
967
|
+
tab = buildPdfTab(entry, "/api/local-image?path=" + encodeURIComponent(Workspace.absPath(entry.path)));
|
|
968
|
+
} else if (kind === "binary") {
|
|
969
|
+
tab = await buildFallbackTab(entry);
|
|
970
|
+
} else if (kind === "table") {
|
|
971
|
+
const text = await Workspace.fetchFileText(entry);
|
|
972
|
+
tab = CodeEditor.looksBinary(text) ? await buildFallbackTab(entry) : buildTableTab(entry, text);
|
|
693
973
|
} else {
|
|
694
974
|
const text = await Workspace.fetchFileText(entry);
|
|
695
|
-
tab = buildTextTab(entry, text);
|
|
975
|
+
tab = CodeEditor.looksBinary(text) ? await buildFallbackTab(entry) : buildTextTab(entry, text);
|
|
696
976
|
}
|
|
697
977
|
} catch (err) {
|
|
698
978
|
console.error("preview failed:", err);
|
|
699
|
-
Modal.toast(t("workspace.previewFailed"), "error");
|
|
979
|
+
Modal.toast(t("workspace.previewFailed") + ": " + err.message, "error");
|
|
700
980
|
return;
|
|
701
981
|
}
|
|
982
|
+
if (tabs.length === 0) ensureAsideWidth();
|
|
702
983
|
tabs.push(tab);
|
|
703
984
|
content.appendChild(tab.el);
|
|
704
985
|
activate(tab.id);
|
|
@@ -709,6 +990,10 @@ const WorkspaceView = (() => {
|
|
|
709
990
|
const tab = activeTab();
|
|
710
991
|
if (tab && tab.isMd) togglePreview(tab);
|
|
711
992
|
});
|
|
993
|
+
splitBtn.addEventListener("click", () => {
|
|
994
|
+
const tab = activeTab();
|
|
995
|
+
if (tab && tab.isMd) toggleSplit(tab);
|
|
996
|
+
});
|
|
712
997
|
copyBtn.addEventListener("click", async () => {
|
|
713
998
|
const tab = activeTab();
|
|
714
999
|
if (!tab || !tab.editor) return;
|
|
@@ -719,16 +1004,13 @@ const WorkspaceView = (() => {
|
|
|
719
1004
|
Modal.toast(t("workspace.copyFailed"), "error");
|
|
720
1005
|
}
|
|
721
1006
|
});
|
|
722
|
-
downloadBtn.addEventListener("click", () => {
|
|
723
|
-
const tab = activeTab();
|
|
724
|
-
if (tab) downloadFile(tab.entry);
|
|
725
|
-
});
|
|
726
1007
|
saveBtn.addEventListener("click", () => {
|
|
727
1008
|
const tab = activeTab();
|
|
728
1009
|
if (tab) doSave(tab);
|
|
729
1010
|
});
|
|
730
1011
|
|
|
731
1012
|
function destroy() {
|
|
1013
|
+
restoreAsideWidth();
|
|
732
1014
|
tabs.forEach((tab) => {
|
|
733
1015
|
if (tab.editor) tab.editor.destroy();
|
|
734
1016
|
if (tab.imageUrl) URL.revokeObjectURL(tab.imageUrl);
|
|
@@ -767,6 +1049,7 @@ const WorkspaceView = (() => {
|
|
|
767
1049
|
const onUp = () => {
|
|
768
1050
|
if (!dragging) return;
|
|
769
1051
|
dragging = false;
|
|
1052
|
+
handle.classList.remove("active");
|
|
770
1053
|
document.body.style.cursor = "";
|
|
771
1054
|
document.body.style.userSelect = "";
|
|
772
1055
|
document.removeEventListener("mousemove", onMove);
|
|
@@ -780,6 +1063,7 @@ const WorkspaceView = (() => {
|
|
|
780
1063
|
dragging = true;
|
|
781
1064
|
startX = e.clientX;
|
|
782
1065
|
startW = treePane.getBoundingClientRect().width;
|
|
1066
|
+
handle.classList.add("active");
|
|
783
1067
|
document.body.style.cursor = "col-resize";
|
|
784
1068
|
document.body.style.userSelect = "none";
|
|
785
1069
|
document.addEventListener("mousemove", onMove);
|
|
@@ -833,10 +1117,22 @@ const WorkspaceView = (() => {
|
|
|
833
1117
|
});
|
|
834
1118
|
tree.addEventListener("mouseleave", () => updateIndentGuides(null));
|
|
835
1119
|
|
|
836
|
-
const viewer = createViewer(
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1120
|
+
const viewer = createViewer(
|
|
1121
|
+
({ count, activeId }) => {
|
|
1122
|
+
root.classList.toggle("has-open-files", count > 0);
|
|
1123
|
+
if (activeId) root.classList.remove("mobile-tree-visible");
|
|
1124
|
+
if (!count) root.classList.remove("mobile-tree-visible");
|
|
1125
|
+
setActiveFilePath(activeId);
|
|
1126
|
+
},
|
|
1127
|
+
() => {
|
|
1128
|
+
const hidden = root.classList.toggle("tree-hidden");
|
|
1129
|
+
return hidden;
|
|
1130
|
+
},
|
|
1131
|
+
() => {
|
|
1132
|
+
root.classList.add("mobile-tree-visible");
|
|
1133
|
+
}
|
|
1134
|
+
);
|
|
1135
|
+
_activeViewer = viewer;
|
|
840
1136
|
|
|
841
1137
|
// ── File search ─────────────────────────────────────────────────────
|
|
842
1138
|
|
|
@@ -970,21 +1266,77 @@ const WorkspaceView = (() => {
|
|
|
970
1266
|
container.appendChild(root);
|
|
971
1267
|
|
|
972
1268
|
loadRoot(tree, viewer.openTab);
|
|
1269
|
+
|
|
1270
|
+
// The tree is otherwise loaded only once per mount. Reload it when the
|
|
1271
|
+
// working directory changes (status-bar dir switch) and when the agent
|
|
1272
|
+
// finishes a task (it may have created/edited files under the dir).
|
|
1273
|
+
if (_activeOff) _activeOff();
|
|
1274
|
+
const offSessionChanged = Workspace.on("workspace:sessionChanged", () => {
|
|
1275
|
+
treeState.indexPromise = null;
|
|
1276
|
+
loadRoot(tree, viewer.openTab);
|
|
1277
|
+
});
|
|
1278
|
+
const offTaskComplete = Workspace.on("workspace:taskCompleted", (payload) => {
|
|
1279
|
+
if (payload && payload.sessionId === Workspace.state.sessionId) {
|
|
1280
|
+
treeState.indexPromise = null;
|
|
1281
|
+
loadRoot(tree, viewer.openTab);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
const offAll = () => { offSessionChanged(); offTaskComplete(); };
|
|
1285
|
+
_activeOff = offAll;
|
|
1286
|
+
|
|
973
1287
|
return {
|
|
974
1288
|
destroy() {
|
|
1289
|
+
if (_activeOff === offAll) {
|
|
1290
|
+
offAll();
|
|
1291
|
+
_activeOff = null;
|
|
1292
|
+
}
|
|
975
1293
|
clearTimeout(searchTimer);
|
|
976
1294
|
treeState.treeEl = null;
|
|
977
1295
|
treeState.activePath = null;
|
|
978
1296
|
treeState.indexPromise = null;
|
|
979
1297
|
lastGuidedRow = null;
|
|
1298
|
+
if (_activeViewer === viewer) _activeViewer = null;
|
|
980
1299
|
viewer.destroy();
|
|
981
1300
|
},
|
|
982
1301
|
};
|
|
983
1302
|
}
|
|
984
1303
|
|
|
985
|
-
|
|
1304
|
+
// Convert an absolute path (from a file:// link in chat) into a tree-style
|
|
1305
|
+
// entry. Paths inside the working directory become relative so tab
|
|
1306
|
+
// de-duplication matches tabs opened from the tree; paths outside stay
|
|
1307
|
+
// absolute (the store treats absolute paths as-is).
|
|
1308
|
+
function toEntry(absPath) {
|
|
1309
|
+
const wd = (Workspace.state.workingDir || "").replace(/\/+$/, "");
|
|
1310
|
+
const name = absPath.split("/").pop() || absPath;
|
|
1311
|
+
if (wd && absPath.startsWith(wd + "/")) {
|
|
1312
|
+
return { name, path: absPath.slice(wd.length + 1), type: "file", size: 0 };
|
|
1313
|
+
}
|
|
1314
|
+
return { name, path: absPath, type: "file", size: 0 };
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Open a file:// link target in the Files tab of the session aside.
|
|
1318
|
+
// Returns true when the viewer handled it; false lets the caller fall back
|
|
1319
|
+
// to the system open action. HTML files open in the preview panel instead
|
|
1320
|
+
// (rendered as a page, not shown as source).
|
|
1321
|
+
function openFile(absPath) {
|
|
1322
|
+
if (!absPath || !Workspace.state.hasSession()) return false;
|
|
1323
|
+
const entry = toEntry(absPath);
|
|
1324
|
+
if (/\.html?$/i.test(entry.name) && window.Clacky && Clacky.Preview) {
|
|
1325
|
+
return Clacky.Preview.open(entry.path);
|
|
1326
|
+
}
|
|
1327
|
+
if (window.Clacky && Clacky.Aside) Clacky.Aside.open();
|
|
1328
|
+
const tabBtn = document.querySelector('.aside-tab[data-tab="files"]');
|
|
1329
|
+
if (tabBtn) tabBtn.click();
|
|
1330
|
+
if (!_activeViewer) return false;
|
|
1331
|
+
_activeViewer.openTab(entry);
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return { mount, openFile };
|
|
986
1336
|
})();
|
|
987
1337
|
|
|
1338
|
+
if (window.Clacky) Clacky.WorkspaceView = WorkspaceView;
|
|
1339
|
+
|
|
988
1340
|
// Files is a built-in tab: visible for every session, after ext-studio
|
|
989
1341
|
// publish (20) and before git (30) / time-machine (40).
|
|
990
1342
|
if (window.Clacky && Clacky.ext) {
|