feat: gaps in tailwind migration closed

This commit is contained in:
2026-05-04 12:27:07 +02:00
parent eca89e51d2
commit 4ab0bc7b4e
24 changed files with 3198 additions and 3005 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
import { clamp } from "../utils/dom.js";
export const applyAppZoom = (nextZoom) => {
const zoom = clamp(Math.round(nextZoom * 100) / 100, 0.5, 2);
window.__bdsAppZoom = zoom;
document.documentElement.style.zoom = String(zoom);
};
export const runDocumentCommand = (command) => {
if (typeof document.execCommand !== "function") {
return false;
}
try {
return document.execCommand(command);
} catch (_error) {
return false;
}
};

View File

@@ -1,5 +1,7 @@
export const createMenuRuntimeCommandRunner = ({ activeMonacoEditor, runMonacoEditorAction, runDocumentCommand, applyAppZoom }) => { import { activeMonacoEditor, runMonacoEditorAction } from "../monaco/services.js";
return (action) => { import { applyAppZoom, runDocumentCommand } from "./document_commands.js";
export const runMenuRuntimeCommand = (action) => {
const editor = activeMonacoEditor(); const editor = activeMonacoEditor();
switch (action) { switch (action) {
@@ -53,5 +55,4 @@ export const createMenuRuntimeCommandRunner = ({ activeMonacoEditor, runMonacoEd
default: default:
return false; return false;
} }
};
}; };

4
assets/js/constants.js Normal file
View File

@@ -0,0 +1,4 @@
export const SIDEBAR_STORAGE_KEY = "bds-panel-sidebar";
export const ASSISTANT_STORAGE_KEY = "bds-panel-assistant-sidebar";
export const UI_LANGUAGE_STORAGE_KEY = "bds-ui-language";
export const WORKBENCH_SESSION_STORAGE_KEY_PREFIX = "bds-workbench-";

View File

@@ -0,0 +1,226 @@
import {
SIDEBAR_STORAGE_KEY,
ASSISTANT_STORAGE_KEY,
UI_LANGUAGE_STORAGE_KEY,
WORKBENCH_SESSION_STORAGE_KEY_PREFIX
} from "../constants.js";
import {
parseJsonObject,
setMediaThumbnailLoaded,
syncMediaThumbnailState,
clamp
} from "../utils/dom.js";
import { shellWidth, setShellWidth, persistWidth, readStoredSize } from "../utils/layout.js";
import {
parseShortcutConfig,
normalizeShortcutKey,
shortcutMatchesEvent,
shortcutTargetIsEditable
} from "../utils/shortcuts.js";
import { syncTitlebarOverlayInsets } from "../bridges/titlebar_overlay.js";
import { runMenuRuntimeCommand } from "../bridges/menu_runtime.js";
export const AppShell = {
mounted() {
this.shortcuts = parseShortcutConfig(this.el.dataset.shortcuts);
this.currentProjectId = this.el.dataset.projectId || "";
this.syncStoredLayout();
this.syncStoredUiLanguage();
this.destroyOverlaySync = syncTitlebarOverlayInsets();
this.workbenchStorageKey = (projectId) =>
projectId ? `${WORKBENCH_SESSION_STORAGE_KEY_PREFIX}${projectId}` : null;
this.restoreStoredWorkbenchSession = () => {
const projectId = this.el.dataset.projectId || "";
const storageKey = this.workbenchStorageKey(projectId);
if (!storageKey) {
return false;
}
const session = parseJsonObject(window.localStorage.getItem(storageKey));
if (!session) {
return false;
}
this.pushEvent("restore_workbench_session", { session });
return true;
};
this.persistWorkbenchSession = () => {
const projectId = this.el.dataset.projectId || "";
const storageKey = this.workbenchStorageKey(projectId);
const session = this.el.dataset.workbenchSession;
if (!storageKey || !session) {
return;
}
window.localStorage.setItem(storageKey, session);
};
this.handleMouseDown = (event) => {
const handle = event.target.closest("[data-role='resize-handle']");
if (!handle || !this.el.contains(handle)) {
return;
}
event.preventDefault();
const target = handle.dataset.resize;
const startX = event.clientX;
const startWidth =
target === "assistant"
? shellWidth("[data-testid='assistant-shell']")
: shellWidth("[data-testid='sidebar-shell']");
const min = target === "assistant" ? 280 : 200;
const max = target === "assistant" ? 640 : 500;
const invert = target === "assistant";
const onMouseMove = (moveEvent) => {
const delta = invert ? startX - moveEvent.clientX : moveEvent.clientX - startX;
const width = clamp(startWidth + delta, min, max);
const selector = target === "assistant" ? "[data-testid='assistant-shell']" : "[data-testid='sidebar-shell']";
setShellWidth(selector, width);
persistWidth(target, width);
};
const onMouseUp = (upEvent) => {
const delta = invert ? startX - upEvent.clientX : upEvent.clientX - startX;
const width = clamp(startWidth + delta, min, max);
persistWidth(target, width);
this.pushEvent("resize_panel", { target, width });
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
};
this.el.addEventListener("mousedown", this.handleMouseDown);
this.handleNativeMenuAction = (event) => {
const action = event.detail?.action;
const ackId = event.detail?.ackId;
if (action) {
this.pushEvent("native_menu_action", { action }, () => {
if (ackId) {
window.dispatchEvent(
new CustomEvent("bds:native-menu-action-ack", { detail: { ackId } })
);
}
});
}
};
this.handleChange = (event) => {
const select = event.target.closest(".status-bar-language-select");
if (select && this.el.contains(select)) {
window.localStorage.setItem(UI_LANGUAGE_STORAGE_KEY, select.value);
}
};
this.handleShortcutKeyDown = (event) => {
if (shortcutTargetIsEditable(event)) {
return;
}
const shortcut = this.shortcuts.find((candidate) => shortcutMatchesEvent(candidate, event));
if (!shortcut) {
return;
}
event.preventDefault();
event.stopPropagation();
this.pushEvent("shortcut", {
key: normalizeShortcutKey(event.key),
meta: event.metaKey,
ctrl: event.ctrlKey,
alt: event.altKey,
shift: event.shiftKey,
tag: event.target?.tagName || null,
contentEditable: event.target?.isContentEditable || false
});
};
this.handleThumbnailLoad = (event) => {
if (event.target instanceof HTMLImageElement && event.target.classList.contains("media-thumbnail-image")) {
setMediaThumbnailLoaded(event.target, true);
}
};
this.handleThumbnailError = (event) => {
if (event.target instanceof HTMLImageElement && event.target.classList.contains("media-thumbnail-image")) {
setMediaThumbnailLoaded(event.target, false);
}
};
this.handleEvent("menu-runtime-command", ({ action }) => {
if (action) {
runMenuRuntimeCommand(String(action));
}
});
window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.addEventListener("keydown", this.handleShortcutKeyDown, true);
this.el.addEventListener("load", this.handleThumbnailLoad, true);
this.el.addEventListener("error", this.handleThumbnailError, true);
this.el.addEventListener("change", this.handleChange);
syncMediaThumbnailState(this.el);
this.restoreStoredWorkbenchSession();
},
updated() {
const nextProjectId = this.el.dataset.projectId || "";
if (nextProjectId !== this.currentProjectId) {
this.currentProjectId = nextProjectId;
if (this.restoreStoredWorkbenchSession()) {
return;
}
}
syncMediaThumbnailState(this.el);
this.persistWorkbenchSession();
},
destroyed() {
this.el.removeEventListener("mousedown", this.handleMouseDown);
this.el.removeEventListener("load", this.handleThumbnailLoad, true);
this.el.removeEventListener("error", this.handleThumbnailError, true);
this.el.removeEventListener("change", this.handleChange);
window.removeEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.removeEventListener("keydown", this.handleShortcutKeyDown, true);
if (this.destroyOverlaySync) {
this.destroyOverlaySync();
}
},
syncStoredLayout() {
this.pushEvent("sync_layout", {
sidebar_width: readStoredSize(SIDEBAR_STORAGE_KEY, shellWidth("[data-testid='sidebar-shell']"), 200, 500),
assistant_sidebar_width: readStoredSize(ASSISTANT_STORAGE_KEY, 360, 280, 640)
});
},
syncStoredUiLanguage() {
const stored = window.localStorage.getItem(UI_LANGUAGE_STORAGE_KEY);
if (stored) {
this.pushEvent("sync_ui_language", { language: stored });
}
}
};

View File

@@ -0,0 +1,139 @@
export const ChatSurface = {
mounted() {
this.stickToBottom = true;
this.scrollContainer = null;
this.autoResize = () => {
const textarea = this.el.querySelector(".chat-input");
if (!textarea) {
return;
}
const styles = getComputedStyle(textarea);
const minHeight = parseFloat(styles.getPropertyValue("--chat-input-min-height")) || 20;
const maxHeight = parseFloat(styles.getPropertyValue("--chat-input-max-height")) || 160;
textarea.rows = 1;
textarea.style.minHeight = `${minHeight}px`;
if (textarea.value.trim() === "") {
textarea.style.height = `${minHeight}px`;
textarea.style.maxHeight = `${minHeight}px`;
textarea.style.overflowY = "hidden";
return;
}
textarea.style.maxHeight = `${maxHeight}px`;
textarea.style.height = "0px";
const nextHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
textarea.style.height = `${nextHeight}px`;
textarea.style.overflowY = nextHeight >= maxHeight ? "auto" : "hidden";
};
this.syncScrollContainer = () => {
const nextContainer = this.el.querySelector(".chat-messages");
if (nextContainer === this.scrollContainer) {
return;
}
if (this.scrollContainer) {
this.scrollContainer.removeEventListener("scroll", this.handleScroll);
}
this.scrollContainer = nextContainer;
if (this.scrollContainer) {
this.scrollContainer.addEventListener("scroll", this.handleScroll);
}
};
this.scrollToBottom = (force = false) => {
if (!this.scrollContainer) {
return;
}
if (force || this.stickToBottom) {
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
}
};
this.syncExpandedSurfaces = () => {
this.el
.querySelectorAll(".chat-inline-surface[data-expanded='true']")
.forEach((surface) => {
surface.open = true;
});
};
this.surfaceObserver = new MutationObserver(() => {
this.syncExpandedSurfaces();
});
this.handleScroll = () => {
if (!this.scrollContainer) {
this.stickToBottom = true;
return;
}
const distanceFromBottom =
this.scrollContainer.scrollHeight -
this.scrollContainer.scrollTop -
this.scrollContainer.clientHeight;
this.stickToBottom = distanceFromBottom < 48;
};
this.handleInput = (event) => {
if (!event.target.closest(".chat-input")) {
return;
}
this.stickToBottom = true;
this.autoResize();
};
this.handleKeyDown = (event) => {
if (!event.target.closest(".chat-input")) {
return;
}
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
event.preventDefault();
const sendButton = this.el.querySelector("[data-testid='chat-send-button']");
if (sendButton && !sendButton.disabled) {
sendButton.click();
}
}
};
this.el.addEventListener("input", this.handleInput);
this.el.addEventListener("keydown", this.handleKeyDown);
this.syncScrollContainer();
this.syncExpandedSurfaces();
this.surfaceObserver.observe(this.el, { childList: true, subtree: true });
this.autoResize();
window.requestAnimationFrame(() => this.scrollToBottom(true));
},
updated() {
this.syncScrollContainer();
this.syncExpandedSurfaces();
this.autoResize();
window.requestAnimationFrame(() => this.scrollToBottom());
},
destroyed() {
this.surfaceObserver.disconnect();
this.el.removeEventListener("input", this.handleInput);
this.el.removeEventListener("keydown", this.handleKeyDown);
if (this.scrollContainer) {
this.scrollContainer.removeEventListener("scroll", this.handleScroll);
}
}
};

View File

@@ -1 +1,18 @@
export const createHooks = (hooks) => hooks; import { AppShell } from "./app_shell.js";
import { SidebarInteractions } from "./sidebar_interactions.js";
import { SettingsSectionScroll, TagsSectionScroll } from "./section_scroll.js";
import { ChatSurface } from "./chat_surface.js";
import { MenuEditorTree } from "./menu_editor_tree.js";
import { MonacoEditor } from "./monaco_editor.js";
import { MonacoDiffEditor } from "./monaco_diff_editor.js";
export const Hooks = {
AppShell,
SidebarInteractions,
SettingsSectionScroll,
TagsSectionScroll,
ChatSurface,
MenuEditorTree,
MonacoEditor,
MonacoDiffEditor
};

View File

@@ -0,0 +1,134 @@
export const MenuEditorTree = {
mounted() {
this.dragItemId = null;
this.dragSourceEl = null;
this.dropTargetEl = null;
this.dropPosition = null;
this.clearDropTarget = () => {
if (this.dropTargetEl) {
this.dropTargetEl.classList.remove("is-drop-before", "is-drop-after", "is-drop-inside");
}
this.dropTargetEl = null;
this.dropPosition = null;
};
this.setDropTarget = (row, position) => {
if (this.dropTargetEl === row && this.dropPosition === position) {
return;
}
this.clearDropTarget();
this.dropTargetEl = row;
this.dropPosition = position;
row.classList.add(`is-drop-${position}`);
};
this.handleDragStart = (event) => {
const handle = event.target.closest("[data-menu-drag-handle='true']");
const row = event.target.closest("[data-menu-item-id]");
if (!handle || !row || !this.el.contains(row)) {
return;
}
this.dragItemId = row.dataset.menuItemId || null;
this.dragSourceEl = row;
row.classList.add("is-dragging");
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", this.dragItemId || "");
}
};
this.handleDragOver = (event) => {
const row = event.target.closest("[data-menu-item-id]");
if (!this.dragItemId || !row || !this.el.contains(row)) {
this.clearDropTarget();
return;
}
const targetItemId = row.dataset.menuItemId || "";
if (!targetItemId || targetItemId === this.dragItemId) {
this.clearDropTarget();
return;
}
event.preventDefault();
const rect = row.getBoundingClientRect();
const offsetY = event.clientY - rect.top;
const allowInside = row.dataset.menuCanDropInside === "true";
const insideBandTop = rect.height * 0.3;
const insideBandBottom = rect.height * 0.7;
const position =
allowInside && offsetY >= insideBandTop && offsetY <= insideBandBottom
? "inside"
: offsetY < rect.height / 2
? "before"
: "after";
this.setDropTarget(row, position);
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
};
this.handleDrop = (event) => {
const row = event.target.closest("[data-menu-item-id]");
if (!this.dragItemId || !row || !this.el.contains(row) || !this.dropPosition) {
this.clearDropTarget();
return;
}
event.preventDefault();
this.pushEvent("menu_editor_drop_item", {
drag_item_id: this.dragItemId,
target_item_id: row.dataset.menuItemId,
position: this.dropPosition
});
this.clearDropTarget();
};
this.handleDragLeave = (event) => {
const related = event.relatedTarget;
if (this.dropTargetEl && (!related || !this.dropTargetEl.contains(related))) {
this.clearDropTarget();
}
};
this.handleDragEnd = () => {
if (this.dragSourceEl) {
this.dragSourceEl.classList.remove("is-dragging");
}
this.dragItemId = null;
this.dragSourceEl = null;
this.clearDropTarget();
};
this.el.addEventListener("dragstart", this.handleDragStart);
this.el.addEventListener("dragover", this.handleDragOver);
this.el.addEventListener("drop", this.handleDrop);
this.el.addEventListener("dragleave", this.handleDragLeave);
this.el.addEventListener("dragend", this.handleDragEnd);
},
destroyed() {
this.el.removeEventListener("dragstart", this.handleDragStart);
this.el.removeEventListener("dragover", this.handleDragOver);
this.el.removeEventListener("drop", this.handleDrop);
this.el.removeEventListener("dragleave", this.handleDragLeave);
this.el.removeEventListener("dragend", this.handleDragEnd);
}
};

View File

@@ -0,0 +1,129 @@
import { loadMonaco, ensureMonacoTheme, diffModelPath } from "../monaco/services.js";
export const MonacoDiffEditor = {
mounted() {
this.host = this.el.querySelector(".monaco-diff-editor-instance");
this.originalInput = this.el.querySelector(".monaco-diff-original");
this.modifiedInput = this.el.querySelector(".monaco-diff-modified");
this.filePath = this.el.dataset.monacoDiffFilePath || "working-tree";
this.language = this.el.dataset.monacoDiffLanguage || "plaintext";
this.viewStyle = this.el.dataset.monacoDiffViewStyle || "inline";
this.wordWrap = this.el.dataset.monacoDiffWordWrap || "off";
this.hideUnchanged = this.el.dataset.monacoDiffHideUnchanged === "true";
this.readValues = () => ({
original: this.originalInput?.value || "",
modified: this.modifiedInput?.value || ""
});
this.applyDataset = () => {
this.filePath = this.el.dataset.monacoDiffFilePath || "working-tree";
this.language = this.el.dataset.monacoDiffLanguage || "plaintext";
this.viewStyle = this.el.dataset.monacoDiffViewStyle || "inline";
this.wordWrap = this.el.dataset.monacoDiffWordWrap || "off";
this.hideUnchanged = this.el.dataset.monacoDiffHideUnchanged === "true";
};
this.setModels = (monaco) => {
const values = this.readValues();
this.originalModel?.dispose();
this.modifiedModel?.dispose();
this.originalModel = monaco.editor.createModel(
values.original,
this.language,
monaco.Uri.parse(diffModelPath(this.filePath, "original"))
);
this.modifiedModel = monaco.editor.createModel(
values.modified,
this.language,
monaco.Uri.parse(diffModelPath(this.filePath, "modified"))
);
this.editor.setModel({ original: this.originalModel, modified: this.modifiedModel });
this.lastFilePath = this.filePath;
};
loadMonaco()
.then((monaco) => {
if (!this.host) {
return;
}
ensureMonacoTheme(monaco);
this.editor = monaco.editor.createDiffEditor(this.host, {
theme: "bds-theme",
automaticLayout: true,
readOnly: true,
renderSideBySide: this.viewStyle === "side-by-side",
minimap: { enabled: false },
scrollBeyondLastLine: false,
lineNumbers: "on",
diffCodeLens: false,
originalEditable: false,
wordWrap: this.wordWrap,
hideUnchangedRegions: { enabled: this.hideUnchanged },
ignoreTrimWhitespace: false
});
this.setModels(monaco);
})
.catch((error) => {
console.error("Failed to load Monaco diff editor", error);
});
},
updated() {
this.host = this.el.querySelector(".monaco-diff-editor-instance");
this.originalInput = this.el.querySelector(".monaco-diff-original");
this.modifiedInput = this.el.querySelector(".monaco-diff-modified");
this.applyDataset();
if (!this.editor) {
return;
}
loadMonaco().then((monaco) => {
ensureMonacoTheme(monaco);
monaco.editor.setTheme("bds-theme");
this.editor.updateOptions({
renderSideBySide: this.viewStyle === "side-by-side",
wordWrap: this.wordWrap,
hideUnchangedRegions: { enabled: this.hideUnchanged }
});
if (this.lastFilePath !== this.filePath) {
this.setModels(monaco);
return;
}
const values = this.readValues();
if (this.originalModel && this.originalModel.getLanguageId() !== this.language) {
monaco.editor.setModelLanguage(this.originalModel, this.language);
}
if (this.modifiedModel && this.modifiedModel.getLanguageId() !== this.language) {
monaco.editor.setModelLanguage(this.modifiedModel, this.language);
}
if (this.originalModel && this.originalModel.getValue() !== values.original) {
this.originalModel.setValue(values.original);
}
if (this.modifiedModel && this.modifiedModel.getValue() !== values.modified) {
this.modifiedModel.setValue(values.modified);
}
});
},
destroyed() {
this.originalModel?.dispose();
this.modifiedModel?.dispose();
this.editor?.dispose();
}
};

View File

@@ -0,0 +1,238 @@
import {
loadMonaco,
ensureMonacoTheme,
registerMonacoEditor,
unregisterMonacoEditor
} from "../monaco/services.js";
export const MonacoEditor = {
mounted() {
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");
this.host = this.el.querySelector(".monaco-editor-instance");
this.language = this.el.dataset.monacoLanguage || "plaintext";
this.wordWrap = this.el.dataset.monacoWordWrap || "off";
this.editorId = this.el.dataset.monacoEditorId || "";
this.insertEvent = this.el.dataset.monacoInsertEvent || "";
this.syncTimer = null;
this.isApplyingRemoteUpdate = false;
this.lastKnownValue = this.textarea?.value || "";
this.syncEditorFromTextarea = () => {
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");
if (!this.textarea || !this.editor) {
return;
}
const value = this.textarea.value || "";
if (this.editor.getValue() !== value) {
this.isApplyingRemoteUpdate = true;
this.editor.setValue(value);
this.isApplyingRemoteUpdate = false;
}
this.lastKnownValue = value;
};
this.layoutEditorSoon = () => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
if (!this.editor) {
return;
}
this.editor.layout();
});
});
};
this.waitForMonacoVisibleSize = () =>
new Promise((resolve) => {
let settled = false;
let attempts = 0;
const hasVisibleSize = () => {
const rect = this.host?.getBoundingClientRect();
return Boolean(rect && rect.width > 0 && rect.height > 0);
};
const finish = () => {
if (settled) {
return;
}
settled = true;
this.visibleSizeObserver?.disconnect();
this.visibleSizeObserver = null;
resolve();
};
const check = () => {
if (hasVisibleSize() || attempts >= 20) {
finish();
return;
}
attempts += 1;
window.requestAnimationFrame(check);
};
if (hasVisibleSize()) {
finish();
return;
}
if (window.ResizeObserver && this.host) {
this.visibleSizeObserver = new ResizeObserver(() => {
if (hasVisibleSize()) {
finish();
}
});
this.visibleSizeObserver.observe(this.host);
}
window.requestAnimationFrame(check);
});
this.queueSync = () => {
if (!this.textarea || !this.editor) {
return;
}
window.clearTimeout(this.syncTimer);
this.syncTimer = window.setTimeout(() => {
if (!this.textarea || !this.editor) {
return;
}
const value = this.editor.getValue();
if (this.textarea.value === value) {
return;
}
this.lastKnownValue = value;
this.textarea.value = value;
this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
}, 120);
};
this.handleInsert = ({ id, content }) => {
if (!this.editor || !content || String(id) !== String(this.editorId)) {
return;
}
const model = this.editor.getModel();
const selection = this.editor.getSelection();
if (!model || !selection) {
return;
}
const value = this.editor.getValue();
const start = model.getOffsetAt(selection.getStartPosition());
const end = model.getOffsetAt(selection.getEndPosition());
const before = value.slice(0, start);
const after = value.slice(end);
const separator = before !== "" && !before.endsWith("\n") ? "\n" : "";
const suffix = after !== "" && !content.endsWith("\n") ? "\n" : "";
const inserted = `${separator}${content}${suffix}`;
this.editor.executeEdits("bds-insert-content", [
{
range: selection,
text: inserted,
forceMoveMarkers: true
}
]);
this.editor.focus();
};
loadMonaco()
.then(async (monaco) => {
if (!this.host || !this.textarea) {
return;
}
await this.waitForMonacoVisibleSize();
ensureMonacoTheme(monaco);
this.editor = monaco.editor.create(this.host, {
value: this.textarea.value || "",
language: this.language,
theme: "bds-theme",
automaticLayout: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: this.wordWrap,
lineNumbers: "on",
lineNumbersMinChars: 3,
fontSize: 14,
fontFamily: "'Cascadia Code', 'Consolas', 'Courier New', monospace",
padding: { top: 12, bottom: 12 },
roundedSelection: false,
renderLineHighlight: "line",
formatOnPaste: true,
cursorStyle: "line",
cursorBlinking: "smooth",
quickSuggestions: this.language === "markdown-with-macros" ? false : true,
tabSize: 2,
insertSpaces: true
});
registerMonacoEditor(this.editorId || this.el.id, this.editor);
monaco.editor.setTheme("bds-theme");
this.syncEditorFromTextarea();
this.layoutEditorSoon();
this.changeSubscription = this.editor.onDidChangeModelContent(() => {
if (this.isApplyingRemoteUpdate) {
return;
}
this.queueSync();
});
if (this.insertEvent) {
this.handleEvent(this.insertEvent, this.handleInsert);
}
})
.catch((error) => {
console.error("Failed to load Monaco editor", error);
});
},
updated() {
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");
this.host = this.el.querySelector(".monaco-editor-instance");
this.language = this.el.dataset.monacoLanguage || this.language || "plaintext";
this.wordWrap = this.el.dataset.monacoWordWrap || this.wordWrap || "off";
if (!this.editor || !this.textarea) {
return;
}
loadMonaco().then((monaco) => {
ensureMonacoTheme(monaco);
monaco.editor.setTheme("bds-theme");
if (this.editor.getModel()?.getLanguageId() !== this.language) {
monaco.editor.setModelLanguage(this.editor.getModel(), this.language);
}
this.editor.updateOptions({ wordWrap: this.wordWrap });
});
this.syncEditorFromTextarea();
this.layoutEditorSoon();
},
destroyed() {
window.clearTimeout(this.syncTimer);
this.visibleSizeObserver?.disconnect();
this.changeSubscription?.dispose();
unregisterMonacoEditor(this.editorId || this.el.id);
this.editor?.dispose();
}
};

View File

@@ -0,0 +1,31 @@
const makeSectionScrollHook = (datasetKey) => ({
mounted() {
this.lastTargetId = null;
this.scrollToSelectedSection();
},
updated() {
this.scrollToSelectedSection();
},
scrollToSelectedSection() {
const targetId = this.el.dataset[datasetKey];
if (!targetId || targetId === this.lastTargetId) {
return;
}
this.lastTargetId = targetId;
window.requestAnimationFrame(() => {
const target = document.getElementById(targetId);
if (target && this.el.contains(target)) {
target.scrollIntoView({ block: "start", behavior: "smooth" });
}
});
}
});
export const SettingsSectionScroll = makeSectionScrollHook("settingsScrollTarget");
export const TagsSectionScroll = makeSectionScrollHook("tagsScrollTarget");

View File

@@ -0,0 +1,24 @@
export const SidebarInteractions = {
mounted() {
this.handleDblClick = (event) => {
const button = event.target.closest("[data-testid='sidebar-open-item']");
if (!button || !this.el.contains(button)) {
return;
}
this.pushEvent("pin_sidebar_item", {
route: button.dataset.route,
id: button.dataset.itemId,
title: button.dataset.openTitle || "",
subtitle: button.dataset.openSubtitle || ""
});
};
this.el.addEventListener("dblclick", this.handleDblClick);
},
destroyed() {
this.el.removeEventListener("dblclick", this.handleDblClick);
}
};

View File

@@ -0,0 +1,145 @@
let liquidLanguageRegistered = false;
let markdownWithMacrosRegistered = false;
export const registerLiquidLanguage = (monaco) => {
if (liquidLanguageRegistered) {
return;
}
monaco.languages.register({ id: "liquid" });
monaco.languages.setLanguageConfiguration("liquid", {
comments: {
blockComment: ["{% comment %}", "{% endcomment %}"]
},
brackets: [
["{", "}"],
["[", "]"],
["(", ")"]
],
autoClosingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" }
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" }
]
});
monaco.languages.setMonarchTokensProvider("liquid", {
defaultToken: "",
tokenizer: {
root: [
[/\{\{-?/, { token: "delimiter.output", next: "@liquidOutput" }],
[/\{%-?\s*comment\b[^%]*-?%\}/, { token: "comment.block", next: "@liquidComment" }],
[/\{%-?/, { token: "delimiter.tag", next: "@liquidTag" }],
[/<!DOCTYPE/i, "metatag"],
[/<!--/, { token: "comment", next: "@htmlComment" }],
[/(<)(script)/i, ["delimiter.html", "tag.html"], "@scriptTag"],
[/(<)(style)/i, ["delimiter.html", "tag.html"], "@styleTag"],
[/(<\/)([\w:-]+)/, ["delimiter.html", "tag.html"]],
[/(<)([\w:-]+)/, ["delimiter.html", "tag.html"], "@htmlTag"],
[/[^<{]+/, ""],
[/./, ""]
],
liquidOutput: [
[/-?\}\}/, { token: "delimiter.output", next: "@pop" }],
[/\|\s*[a-zA-Z_][\w-]*/, "keyword"],
[/\b(?:true|false|nil|blank|empty)\b/, "keyword"],
[/\b\d+(?:\.\d+)?\b/, "number"],
[/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/, "string"],
[/[a-zA-Z_][\w.-]*/, "identifier"],
[/[,:()[\]]/, "delimiter"]
],
liquidTag: [
[/-?%\}/, { token: "delimiter.tag", next: "@pop" }],
[/\b(?:assign|capture|case|comment|cycle|decrement|echo|elsif|else|endcase|endcapture|endif|endfor|endunless|endcomment|for|if|include|increment|liquid|paginate|raw|render|tablerow|unless|when)\b/, "keyword"],
[/\|\s*[a-zA-Z_][\w-]*/, "keyword"],
[/\b(?:true|false|nil|blank|empty|contains)\b/, "keyword"],
[/\b\d+(?:\.\d+)?\b/, "number"],
[/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/, "string"],
[/[><=!]=?|\.|:/, "operator"],
[/[a-zA-Z_][\w.-]*/, "identifier"],
[/[,:()[\]]/, "delimiter"]
],
liquidComment: [
[/\{%-?\s*endcomment\s*-?%\}/, { token: "comment.block", next: "@pop" }],
[/./, "comment.block"]
],
htmlComment: [
[/-->/, { token: "comment", next: "@pop" }],
[/./, "comment"]
],
htmlTag: [
[/\/>/, { token: "delimiter.html", next: "@pop" }],
[/>/, { token: "delimiter.html", next: "@pop" }],
[/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/, "attribute.value"],
[/[\w:-]+/, "attribute.name"],
[/=/, "delimiter"]
],
scriptTag: [
[/>/, { token: "delimiter.html", next: "@pop" }],
[/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/, "attribute.value"],
[/[\w:-]+/, "attribute.name"],
[/=/, "delimiter"]
],
styleTag: [
[/>/, { token: "delimiter.html", next: "@pop" }],
[/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/, "attribute.value"],
[/[\w:-]+/, "attribute.name"],
[/=/, "delimiter"]
]
}
});
liquidLanguageRegistered = true;
};
export const registerMarkdownWithMacrosLanguage = (monaco) => {
if (markdownWithMacrosRegistered) {
return;
}
monaco.languages.register({ id: "markdown-with-macros" });
monaco.languages.setMonarchTokensProvider("markdown-with-macros", {
defaultToken: "",
tokenPostfix: ".md",
tokenizer: {
root: [
[/\[\[[a-zA-Z][\w-]*/, { token: "keyword.macro", next: "@macroParams" }],
[/^#{1,6}\s.*$/, "keyword.header"],
[/^\s*>+/, "string.quote"],
[/^\s*[-+*]\s/, "keyword"],
[/^\s*\d+\.\s/, "keyword"],
[/^\s*```\w*/, { token: "string.code", next: "@codeblock" }],
[/\*\*[^*]+\*\*/, "strong"],
[/\*[^*]+\*/, "emphasis"],
[/__[^_]+__/, "strong"],
[/_[^_]+_/, "emphasis"],
[/`[^`]+`/, "variable"],
[/!?\[[^\]]*\]\([^)]*\)/, "string.link"],
[/!?\[[^\]]*\]\[[^\]]*\]/, "string.link"]
],
macroParams: [
[/\]\]/, { token: "keyword.macro", next: "@root" }],
[/[a-zA-Z][\w-]*(?=\s*=)/, "attribute.name"],
[/=/, "delimiter"],
[/"[^"]*"/, "string"],
[/\s+/, "white"],
[/[^\]"=\s]+/, "attribute.value"]
],
codeblock: [
[/^\s*```\s*$/, { token: "string.code", next: "@root" }],
[/.*$/, "variable.source"]
]
}
});
markdownWithMacrosRegistered = true;
};

View File

@@ -1 +1,88 @@
export const createMonacoServices = (services) => services; import { loadScript } from "../utils/script_loader.js";
import { ensureMonacoTheme } from "./theme.js";
import { registerLiquidLanguage, registerMarkdownWithMacrosLanguage } from "./languages.js";
let monacoLoaderPromise;
const monacoEditors = new Map();
export const loadMonaco = () => {
if (window.monaco?.editor) {
ensureMonacoTheme(window.monaco);
registerLiquidLanguage(window.monaco);
registerMarkdownWithMacrosLanguage(window.monaco);
return Promise.resolve(window.monaco);
}
if (monacoLoaderPromise) {
return monacoLoaderPromise;
}
monacoLoaderPromise = loadScript("/monaco/vs/loader.js")
.then(
() =>
new Promise((resolve, reject) => {
window.require.config({ paths: { vs: "/monaco/vs" } });
window.require(["vs/editor/editor.main"], () => {
ensureMonacoTheme(window.monaco);
registerLiquidLanguage(window.monaco);
registerMarkdownWithMacrosLanguage(window.monaco);
resolve(window.monaco);
}, reject);
})
)
.catch((error) => {
monacoLoaderPromise = null;
throw error;
});
return monacoLoaderPromise;
};
export const registerMonacoEditor = (key, editor) => {
if (key) {
monacoEditors.set(key, editor);
}
};
export const unregisterMonacoEditor = (key) => {
if (key) {
monacoEditors.delete(key);
}
};
export const activeMonacoEditor = () => {
for (const editor of monacoEditors.values()) {
if (typeof editor?.hasTextFocus === "function" && editor.hasTextFocus()) {
return editor;
}
}
return null;
};
export const runMonacoEditorAction = (editor, actionId, triggerId = actionId) => {
if (!editor) {
return false;
}
const action = typeof editor.getAction === "function" ? editor.getAction(actionId) : null;
if (action && typeof action.run === "function") {
action.run();
return true;
}
if (typeof editor.trigger === "function") {
editor.trigger("bds-menu", triggerId, null);
return true;
}
return false;
};
export const diffModelPath = (filePath, side) => {
const normalized = String(filePath || "working-tree").replace(/^\/+/, "");
return `inmemory://model/git-diff/${side}/${normalized}`;
};
export { ensureMonacoTheme };

62
assets/js/monaco/theme.js Normal file
View File

@@ -0,0 +1,62 @@
import { cssVar, normalizeMonacoColor } from "../utils/color.js";
let monacoThemeSignature = null;
export const ensureMonacoTheme = (monaco) => {
const background = normalizeMonacoColor(
cssVar("--vscode-editor-background", cssVar("--vscode-input-background", "#1e1e1e")),
"#1e1e1e"
);
const foreground = normalizeMonacoColor(cssVar("--vscode-editor-foreground", "#d4d4d4"), "#d4d4d4");
const lineNumber = normalizeMonacoColor(cssVar("--vscode-editorLineNumber-foreground", "#858585"), "#858585");
const activeLineNumber = normalizeMonacoColor(
cssVar("--vscode-editorLineNumber-activeForeground", foreground),
foreground
);
const selection = normalizeMonacoColor(cssVar("--vscode-editor-selectionBackground", "#264f78"), "#264f78");
const inactiveSelection = normalizeMonacoColor(
cssVar("--vscode-editor-inactiveSelectionBackground", "#3a3d41"),
"#3a3d41"
);
const cursor = normalizeMonacoColor(cssVar("--vscode-editorCursor-foreground", foreground), foreground);
const border = normalizeMonacoColor(cssVar("--vscode-panel-border", "#3c3c3c"), "#3c3c3c");
const lineHighlight = normalizeMonacoColor(
cssVar("--vscode-editor-lineHighlightBackground", background),
background
);
const signature = [background, foreground, lineNumber, activeLineNumber, selection, inactiveSelection, cursor, border].join("|");
if (signature === monacoThemeSignature) {
monaco.editor.setTheme("bds-theme");
return;
}
monaco.editor.defineTheme("bds-theme", {
base: "vs-dark",
inherit: true,
rules: [
{ token: "keyword.macro", foreground: "C586C0", fontStyle: "bold" },
{ token: "attribute.name", foreground: "9CDCFE" },
{ token: "attribute.value", foreground: "CE9178" }
],
colors: {
"editor.background": background,
"editor.foreground": foreground,
"editor.lineHighlightBackground": lineHighlight,
"editorCursor.foreground": cursor,
"editor.selectionBackground": selection,
"editor.inactiveSelectionBackground": inactiveSelection,
"editorLineNumber.foreground": lineNumber,
"editorLineNumber.activeForeground": activeLineNumber,
"editorIndentGuide.background1": border,
"editorIndentGuide.activeBackground1": foreground,
"editorWidget.border": border,
"editorGutter.background": background,
"focusBorder": border,
"input.border": border
}
});
monacoThemeSignature = signature;
monaco.editor.setTheme("bds-theme");
};

46
assets/js/utils/color.js Normal file
View File

@@ -0,0 +1,46 @@
import { clamp } from "./dom.js";
export const cssVar = (name, fallback) => {
const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return value || fallback;
};
const parseRgbColor = (value) => {
if (!value) {
return null;
}
const hex = value.match(/^#([0-9a-f]{6})$/i);
if (hex) {
return {
r: Number.parseInt(hex[1].slice(0, 2), 16),
g: Number.parseInt(hex[1].slice(2, 4), 16),
b: Number.parseInt(hex[1].slice(4, 6), 16)
};
}
const rgb = value.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (!rgb) {
return null;
}
return {
r: Number.parseInt(rgb[1], 10),
g: Number.parseInt(rgb[2], 10),
b: Number.parseInt(rgb[3], 10)
};
};
export const normalizeMonacoColor = (value, fallback) => {
const rgb = parseRgbColor(value);
if (!rgb) {
return fallback;
}
return `#${[rgb.r, rgb.g, rgb.b]
.map((channel) => clamp(channel, 0, 255).toString(16).padStart(2, "0"))
.join("")}`;
};

34
assets/js/utils/dom.js Normal file
View File

@@ -0,0 +1,34 @@
export const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
export const parseJsonObject = (value) => {
if (!value) {
return null;
}
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch (_error) {
return null;
}
};
export const setMediaThumbnailLoaded = (image, loaded) => {
const thumbnail = image?.closest(".media-thumbnail");
if (!thumbnail) {
return;
}
if (loaded) {
thumbnail.classList.add("is-loaded");
} else {
thumbnail.classList.remove("is-loaded");
}
};
export const syncMediaThumbnailState = (root) => {
root.querySelectorAll(".media-thumbnail-image").forEach((image) => {
setMediaThumbnailLoaded(image, Boolean(image.complete && image.naturalWidth > 0));
});
};

43
assets/js/utils/layout.js Normal file
View File

@@ -0,0 +1,43 @@
import { clamp } from "./dom.js";
import { SIDEBAR_STORAGE_KEY, ASSISTANT_STORAGE_KEY } from "../constants.js";
export const shellWidth = (selector) => {
const shell = document.querySelector(selector);
if (!shell) {
return 0;
}
const width = Number.parseInt(shell.style.width || "0", 10);
return Number.isNaN(width) ? Math.round(shell.getBoundingClientRect().width) : width;
};
export const setShellWidth = (selector, width) => {
const shell = document.querySelector(selector);
if (shell) {
shell.style.width = `${width}px`;
shell.classList.remove("is-hidden");
}
};
export const persistWidth = (target, width) => {
const key = target === "assistant" ? ASSISTANT_STORAGE_KEY : SIDEBAR_STORAGE_KEY;
window.localStorage.setItem(key, String(width));
};
export const readStoredSize = (key, fallback, min, max) => {
const raw = window.localStorage.getItem(key);
if (!raw) {
return fallback;
}
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) {
return fallback;
}
return clamp(parsed, min, max);
};

View File

@@ -0,0 +1,33 @@
export const loadScript = (src) =>
new Promise((resolve, reject) => {
const existing = document.querySelector(`script[src="${src}"]`);
if (existing) {
if (existing.dataset.loaded === "true") {
resolve();
return;
}
existing.addEventListener("load", () => resolve(), { once: true });
existing.addEventListener("error", () => reject(new Error(`Failed to load ${src}`)), {
once: true
});
return;
}
const script = document.createElement("script");
script.src = src;
script.async = true;
script.addEventListener(
"load",
() => {
script.dataset.loaded = "true";
resolve();
},
{ once: true }
);
script.addEventListener("error", () => reject(new Error(`Failed to load ${src}`)), {
once: true
});
document.head.appendChild(script);
});

View File

@@ -0,0 +1,30 @@
export const normalizeShortcutKey = (key) => String(key || "").toLowerCase();
export const shortcutTargetIsEditable = (event) => {
const tag = event.target?.tagName || null;
return event.target?.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(tag);
};
export const shortcutMatchesEvent = (shortcut, event) => {
const primary = event.metaKey || event.ctrlKey;
return (
normalizeShortcutKey(event.key) === normalizeShortcutKey(shortcut.key) &&
primary === Boolean(shortcut.primary) &&
event.shiftKey === Boolean(shortcut.shift) &&
event.altKey === Boolean(shortcut.alt)
);
};
export const parseShortcutConfig = (value) => {
if (!value) {
return [];
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch (_error) {
return [];
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -8405,6 +8405,96 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}, false); }, false);
})(); })();
// js/constants.js
var SIDEBAR_STORAGE_KEY = "bds-panel-sidebar";
var ASSISTANT_STORAGE_KEY = "bds-panel-assistant-sidebar";
var UI_LANGUAGE_STORAGE_KEY = "bds-ui-language";
var WORKBENCH_SESSION_STORAGE_KEY_PREFIX = "bds-workbench-";
// js/utils/dom.js
var clamp = (value, min, max) => Math.max(min, Math.min(value, max));
var parseJsonObject = (value) => {
if (!value) {
return null;
}
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch (_error) {
return null;
}
};
var setMediaThumbnailLoaded = (image, loaded) => {
const thumbnail = image?.closest(".media-thumbnail");
if (!thumbnail) {
return;
}
if (loaded) {
thumbnail.classList.add("is-loaded");
} else {
thumbnail.classList.remove("is-loaded");
}
};
var syncMediaThumbnailState = (root) => {
root.querySelectorAll(".media-thumbnail-image").forEach((image) => {
setMediaThumbnailLoaded(image, Boolean(image.complete && image.naturalWidth > 0));
});
};
// js/utils/layout.js
var shellWidth = (selector) => {
const shell = document.querySelector(selector);
if (!shell) {
return 0;
}
const width = Number.parseInt(shell.style.width || "0", 10);
return Number.isNaN(width) ? Math.round(shell.getBoundingClientRect().width) : width;
};
var setShellWidth = (selector, width) => {
const shell = document.querySelector(selector);
if (shell) {
shell.style.width = `${width}px`;
shell.classList.remove("is-hidden");
}
};
var persistWidth = (target, width) => {
const key = target === "assistant" ? ASSISTANT_STORAGE_KEY : SIDEBAR_STORAGE_KEY;
window.localStorage.setItem(key, String(width));
};
var readStoredSize = (key, fallback, min, max) => {
const raw = window.localStorage.getItem(key);
if (!raw) {
return fallback;
}
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) {
return fallback;
}
return clamp(parsed, min, max);
};
// js/utils/shortcuts.js
var normalizeShortcutKey = (key) => String(key || "").toLowerCase();
var shortcutTargetIsEditable = (event) => {
const tag = event.target?.tagName || null;
return event.target?.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(tag);
};
var shortcutMatchesEvent = (shortcut, event) => {
const primary = event.metaKey || event.ctrlKey;
return normalizeShortcutKey(event.key) === normalizeShortcutKey(shortcut.key) && primary === Boolean(shortcut.primary) && event.shiftKey === Boolean(shortcut.shift) && event.altKey === Boolean(shortcut.alt);
};
var parseShortcutConfig = (value) => {
if (!value) {
return [];
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch (_error) {
return [];
}
};
// js/bridges/titlebar_overlay.js // js/bridges/titlebar_overlay.js
var syncTitlebarOverlayInsets = () => { var syncTitlebarOverlayInsets = () => {
const rootStyle = document.documentElement.style; const rootStyle = document.documentElement.style;
@@ -8440,229 +8530,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}; };
}; };
// js/bridges/menu_runtime.js // js/utils/script_loader.js
var createMenuRuntimeCommandRunner = ({ activeMonacoEditor, runMonacoEditorAction, runDocumentCommand, applyAppZoom }) => { var loadScript = (src) => new Promise((resolve, reject) => {
return (action) => {
const editor = activeMonacoEditor();
switch (action) {
case "undo":
return editor ? runMonacoEditorAction(editor, "undo") : runDocumentCommand("undo");
case "redo":
return editor ? runMonacoEditorAction(editor, "redo") : runDocumentCommand("redo");
case "cut":
return editor ? runMonacoEditorAction(editor, "editor.action.clipboardCutAction") : runDocumentCommand("cut");
case "copy":
return editor ? runMonacoEditorAction(editor, "editor.action.clipboardCopyAction") : runDocumentCommand("copy");
case "paste":
return editor ? runMonacoEditorAction(editor, "editor.action.clipboardPasteAction") : runDocumentCommand("paste");
case "delete":
return editor ? runMonacoEditorAction(editor, "deleteLeft") : runDocumentCommand("delete");
case "select_all":
return editor ? runMonacoEditorAction(editor, "editor.action.selectAll") : runDocumentCommand("selectAll");
case "find":
return editor ? runMonacoEditorAction(editor, "actions.find") : false;
case "replace":
return editor ? runMonacoEditorAction(editor, "editor.action.startFindReplaceAction") : false;
case "reload":
case "force_reload":
window.location.reload();
return true;
case "reset_zoom":
applyAppZoom(1);
return true;
case "zoom_in":
applyAppZoom((window.__bdsAppZoom || 1) + 0.1);
return true;
case "zoom_out":
applyAppZoom((window.__bdsAppZoom || 1) - 0.1);
return true;
case "toggle_full_screen":
if (document.fullscreenElement) {
document.exitFullscreen?.();
} else {
document.documentElement.requestFullscreen?.();
}
return true;
default:
return false;
}
};
};
// js/hooks/index.js
var createHooks = (hooks) => hooks;
// js/monaco/services.js
var createMonacoServices = (services) => services;
// js/app.js
document.addEventListener("DOMContentLoaded", () => {
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const SIDEBAR_STORAGE_KEY = "bds-panel-sidebar";
const ASSISTANT_STORAGE_KEY = "bds-panel-assistant-sidebar";
const UI_LANGUAGE_STORAGE_KEY = "bds-ui-language";
const WORKBENCH_SESSION_STORAGE_KEY_PREFIX = "bds-workbench-";
const parseShortcutConfig = (value) => {
if (!value) {
return [];
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch (_error) {
return [];
}
};
const parseJsonObject = (value) => {
if (!value) {
return null;
}
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch (_error) {
return null;
}
};
const setMediaThumbnailLoaded = (image, loaded) => {
const thumbnail = image?.closest(".media-thumbnail");
if (!thumbnail) {
return;
}
if (loaded) {
thumbnail.classList.add("is-loaded");
} else {
thumbnail.classList.remove("is-loaded");
}
};
const syncMediaThumbnailState = (root) => {
root.querySelectorAll(".media-thumbnail-image").forEach((image) => {
setMediaThumbnailLoaded(image, Boolean(image.complete && image.naturalWidth > 0));
});
};
const normalizeShortcutKey = (key) => String(key || "").toLowerCase();
const shortcutTargetIsEditable = (event) => {
const tag = event.target?.tagName || null;
return event.target?.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(tag);
};
const shortcutMatchesEvent = (shortcut, event) => {
const primary = event.metaKey || event.ctrlKey;
return normalizeShortcutKey(event.key) === normalizeShortcutKey(shortcut.key) && primary === Boolean(shortcut.primary) && event.shiftKey === Boolean(shortcut.shift) && event.altKey === Boolean(shortcut.alt);
};
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
const readStoredSize = (key, fallback, min, max) => {
const raw = window.localStorage.getItem(key);
if (!raw) {
return fallback;
}
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) {
return fallback;
}
return clamp(parsed, min, max);
};
const shellWidth = (selector) => {
const shell = document.querySelector(selector);
if (!shell) {
return 0;
}
const width = Number.parseInt(shell.style.width || "0", 10);
return Number.isNaN(width) ? Math.round(shell.getBoundingClientRect().width) : width;
};
const setShellWidth = (selector, width) => {
const shell = document.querySelector(selector);
if (shell) {
shell.style.width = `${width}px`;
shell.classList.remove("is-hidden");
}
};
const persistWidth = (target, width) => {
const key = target === "assistant" ? ASSISTANT_STORAGE_KEY : SIDEBAR_STORAGE_KEY;
window.localStorage.setItem(key, String(width));
};
let monacoLoaderPromise;
let liquidLanguageRegistered = false;
let markdownWithMacrosRegistered = false;
let monacoThemeSignature = null;
const monacoEditors = /* @__PURE__ */ new Map();
const activeMonacoEditor = () => {
for (const editor of monacoEditors.values()) {
if (typeof editor?.hasTextFocus === "function" && editor.hasTextFocus()) {
return editor;
}
}
return null;
};
const runMonacoEditorAction = (editor, actionId, triggerId = actionId) => {
if (!editor) {
return false;
}
const action = typeof editor.getAction === "function" ? editor.getAction(actionId) : null;
if (action && typeof action.run === "function") {
action.run();
return true;
}
if (typeof editor.trigger === "function") {
editor.trigger("bds-menu", triggerId, null);
return true;
}
return false;
};
const runDocumentCommand = (command) => {
if (typeof document.execCommand !== "function") {
return false;
}
try {
return document.execCommand(command);
} catch (_error) {
return false;
}
};
const applyAppZoom = (nextZoom) => {
const zoom = clamp(Math.round(nextZoom * 100) / 100, 0.5, 2);
window.__bdsAppZoom = zoom;
document.documentElement.style.zoom = String(zoom);
};
const menuRuntimeCommandRunner = createMenuRuntimeCommandRunner({
activeMonacoEditor,
runMonacoEditorAction,
runDocumentCommand,
applyAppZoom
});
const cssVar = (name, fallback) => {
const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return value || fallback;
};
const parseRgbColor = (value) => {
if (!value) {
return null;
}
const hex = value.match(/^#([0-9a-f]{6})$/i);
if (hex) {
return {
r: Number.parseInt(hex[1].slice(0, 2), 16),
g: Number.parseInt(hex[1].slice(2, 4), 16),
b: Number.parseInt(hex[1].slice(4, 6), 16)
};
}
const rgb = value.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (!rgb) {
return null;
}
return {
r: Number.parseInt(rgb[1], 10),
g: Number.parseInt(rgb[2], 10),
b: Number.parseInt(rgb[3], 10)
};
};
const normalizeMonacoColor = (value, fallback) => {
const rgb = parseRgbColor(value);
if (!rgb) {
return fallback;
}
return `#${[rgb.r, rgb.g, rgb.b].map((channel) => clamp(channel, 0, 255).toString(16).padStart(2, "0")).join("")}`;
};
const loadScript = (src) => new Promise((resolve, reject) => {
const existing = document.querySelector(`script[src="${src}"]`); const existing = document.querySelector(`script[src="${src}"]`);
if (existing) { if (existing) {
if (existing.dataset.loaded === "true") { if (existing.dataset.loaded === "true") {
@@ -8691,11 +8560,104 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}); });
document.head.appendChild(script); document.head.appendChild(script);
}); });
const diffModelPath = (filePath, side) => {
const normalized = String(filePath || "working-tree").replace(/^\/+/, ""); // js/utils/color.js
return `inmemory://model/git-diff/${side}/${normalized}`; var cssVar = (name, fallback) => {
const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return value || fallback;
}; };
const registerLiquidLanguage = (monaco) => { var parseRgbColor = (value) => {
if (!value) {
return null;
}
const hex = value.match(/^#([0-9a-f]{6})$/i);
if (hex) {
return {
r: Number.parseInt(hex[1].slice(0, 2), 16),
g: Number.parseInt(hex[1].slice(2, 4), 16),
b: Number.parseInt(hex[1].slice(4, 6), 16)
};
}
const rgb = value.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (!rgb) {
return null;
}
return {
r: Number.parseInt(rgb[1], 10),
g: Number.parseInt(rgb[2], 10),
b: Number.parseInt(rgb[3], 10)
};
};
var normalizeMonacoColor = (value, fallback) => {
const rgb = parseRgbColor(value);
if (!rgb) {
return fallback;
}
return `#${[rgb.r, rgb.g, rgb.b].map((channel) => clamp(channel, 0, 255).toString(16).padStart(2, "0")).join("")}`;
};
// js/monaco/theme.js
var monacoThemeSignature = null;
var ensureMonacoTheme = (monaco) => {
const background = normalizeMonacoColor(
cssVar("--vscode-editor-background", cssVar("--vscode-input-background", "#1e1e1e")),
"#1e1e1e"
);
const foreground = normalizeMonacoColor(cssVar("--vscode-editor-foreground", "#d4d4d4"), "#d4d4d4");
const lineNumber = normalizeMonacoColor(cssVar("--vscode-editorLineNumber-foreground", "#858585"), "#858585");
const activeLineNumber = normalizeMonacoColor(
cssVar("--vscode-editorLineNumber-activeForeground", foreground),
foreground
);
const selection = normalizeMonacoColor(cssVar("--vscode-editor-selectionBackground", "#264f78"), "#264f78");
const inactiveSelection = normalizeMonacoColor(
cssVar("--vscode-editor-inactiveSelectionBackground", "#3a3d41"),
"#3a3d41"
);
const cursor = normalizeMonacoColor(cssVar("--vscode-editorCursor-foreground", foreground), foreground);
const border = normalizeMonacoColor(cssVar("--vscode-panel-border", "#3c3c3c"), "#3c3c3c");
const lineHighlight = normalizeMonacoColor(
cssVar("--vscode-editor-lineHighlightBackground", background),
background
);
const signature = [background, foreground, lineNumber, activeLineNumber, selection, inactiveSelection, cursor, border].join("|");
if (signature === monacoThemeSignature) {
monaco.editor.setTheme("bds-theme");
return;
}
monaco.editor.defineTheme("bds-theme", {
base: "vs-dark",
inherit: true,
rules: [
{ token: "keyword.macro", foreground: "C586C0", fontStyle: "bold" },
{ token: "attribute.name", foreground: "9CDCFE" },
{ token: "attribute.value", foreground: "CE9178" }
],
colors: {
"editor.background": background,
"editor.foreground": foreground,
"editor.lineHighlightBackground": lineHighlight,
"editorCursor.foreground": cursor,
"editor.selectionBackground": selection,
"editor.inactiveSelectionBackground": inactiveSelection,
"editorLineNumber.foreground": lineNumber,
"editorLineNumber.activeForeground": activeLineNumber,
"editorIndentGuide.background1": border,
"editorIndentGuide.activeBackground1": foreground,
"editorWidget.border": border,
"editorGutter.background": background,
"focusBorder": border,
"input.border": border
}
});
monacoThemeSignature = signature;
monaco.editor.setTheme("bds-theme");
};
// js/monaco/languages.js
var liquidLanguageRegistered = false;
var markdownWithMacrosRegistered = false;
var registerLiquidLanguage = (monaco) => {
if (liquidLanguageRegistered) { if (liquidLanguageRegistered) {
return; return;
} }
@@ -8791,7 +8753,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}); });
liquidLanguageRegistered = true; liquidLanguageRegistered = true;
}; };
const registerMarkdownWithMacrosLanguage = (monaco) => { var registerMarkdownWithMacrosLanguage = (monaco) => {
if (markdownWithMacrosRegistered) { if (markdownWithMacrosRegistered) {
return; return;
} }
@@ -8831,62 +8793,11 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}); });
markdownWithMacrosRegistered = true; markdownWithMacrosRegistered = true;
}; };
const ensureMonacoTheme = (monaco) => {
const background = normalizeMonacoColor( // js/monaco/services.js
cssVar("--vscode-editor-background", cssVar("--vscode-input-background", "#1e1e1e")), var monacoLoaderPromise;
"#1e1e1e" var monacoEditors = /* @__PURE__ */ new Map();
); var loadMonaco = () => {
const foreground = normalizeMonacoColor(cssVar("--vscode-editor-foreground", "#d4d4d4"), "#d4d4d4");
const lineNumber = normalizeMonacoColor(cssVar("--vscode-editorLineNumber-foreground", "#858585"), "#858585");
const activeLineNumber = normalizeMonacoColor(
cssVar("--vscode-editorLineNumber-activeForeground", foreground),
foreground
);
const selection = normalizeMonacoColor(cssVar("--vscode-editor-selectionBackground", "#264f78"), "#264f78");
const inactiveSelection = normalizeMonacoColor(
cssVar("--vscode-editor-inactiveSelectionBackground", "#3a3d41"),
"#3a3d41"
);
const cursor = normalizeMonacoColor(cssVar("--vscode-editorCursor-foreground", foreground), foreground);
const border = normalizeMonacoColor(cssVar("--vscode-panel-border", "#3c3c3c"), "#3c3c3c");
const lineHighlight = normalizeMonacoColor(
cssVar("--vscode-editor-lineHighlightBackground", background),
background
);
const signature = [background, foreground, lineNumber, activeLineNumber, selection, inactiveSelection, cursor, border].join("|");
if (signature === monacoThemeSignature) {
monaco.editor.setTheme("bds-theme");
return;
}
monaco.editor.defineTheme("bds-theme", {
base: "vs-dark",
inherit: true,
rules: [
{ token: "keyword.macro", foreground: "C586C0", fontStyle: "bold" },
{ token: "attribute.name", foreground: "9CDCFE" },
{ token: "attribute.value", foreground: "CE9178" }
],
colors: {
"editor.background": background,
"editor.foreground": foreground,
"editor.lineHighlightBackground": lineHighlight,
"editorCursor.foreground": cursor,
"editor.selectionBackground": selection,
"editor.inactiveSelectionBackground": inactiveSelection,
"editorLineNumber.foreground": lineNumber,
"editorLineNumber.activeForeground": activeLineNumber,
"editorIndentGuide.background1": border,
"editorIndentGuide.activeBackground1": foreground,
"editorWidget.border": border,
"editorGutter.background": background,
"focusBorder": border,
"input.border": border
}
});
monacoThemeSignature = signature;
monaco.editor.setTheme("bds-theme");
};
const loadMonaco = () => {
if (window.monaco?.editor) { if (window.monaco?.editor) {
ensureMonacoTheme(window.monaco); ensureMonacoTheme(window.monaco);
registerLiquidLanguage(window.monaco); registerLiquidLanguage(window.monaco);
@@ -8912,9 +8823,110 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}); });
return monacoLoaderPromise; return monacoLoaderPromise;
}; };
const monacoServices = createMonacoServices({ loadMonaco, ensureMonacoTheme }); var registerMonacoEditor = (key, editor) => {
const Hooks2 = { if (key) {
AppShell: { monacoEditors.set(key, editor);
}
};
var unregisterMonacoEditor = (key) => {
if (key) {
monacoEditors.delete(key);
}
};
var activeMonacoEditor = () => {
for (const editor of monacoEditors.values()) {
if (typeof editor?.hasTextFocus === "function" && editor.hasTextFocus()) {
return editor;
}
}
return null;
};
var runMonacoEditorAction = (editor, actionId, triggerId = actionId) => {
if (!editor) {
return false;
}
const action = typeof editor.getAction === "function" ? editor.getAction(actionId) : null;
if (action && typeof action.run === "function") {
action.run();
return true;
}
if (typeof editor.trigger === "function") {
editor.trigger("bds-menu", triggerId, null);
return true;
}
return false;
};
var diffModelPath = (filePath, side) => {
const normalized = String(filePath || "working-tree").replace(/^\/+/, "");
return `inmemory://model/git-diff/${side}/${normalized}`;
};
// js/bridges/document_commands.js
var applyAppZoom = (nextZoom) => {
const zoom = clamp(Math.round(nextZoom * 100) / 100, 0.5, 2);
window.__bdsAppZoom = zoom;
document.documentElement.style.zoom = String(zoom);
};
var runDocumentCommand = (command) => {
if (typeof document.execCommand !== "function") {
return false;
}
try {
return document.execCommand(command);
} catch (_error) {
return false;
}
};
// js/bridges/menu_runtime.js
var runMenuRuntimeCommand = (action) => {
const editor = activeMonacoEditor();
switch (action) {
case "undo":
return editor ? runMonacoEditorAction(editor, "undo") : runDocumentCommand("undo");
case "redo":
return editor ? runMonacoEditorAction(editor, "redo") : runDocumentCommand("redo");
case "cut":
return editor ? runMonacoEditorAction(editor, "editor.action.clipboardCutAction") : runDocumentCommand("cut");
case "copy":
return editor ? runMonacoEditorAction(editor, "editor.action.clipboardCopyAction") : runDocumentCommand("copy");
case "paste":
return editor ? runMonacoEditorAction(editor, "editor.action.clipboardPasteAction") : runDocumentCommand("paste");
case "delete":
return editor ? runMonacoEditorAction(editor, "deleteLeft") : runDocumentCommand("delete");
case "select_all":
return editor ? runMonacoEditorAction(editor, "editor.action.selectAll") : runDocumentCommand("selectAll");
case "find":
return editor ? runMonacoEditorAction(editor, "actions.find") : false;
case "replace":
return editor ? runMonacoEditorAction(editor, "editor.action.startFindReplaceAction") : false;
case "reload":
case "force_reload":
window.location.reload();
return true;
case "reset_zoom":
applyAppZoom(1);
return true;
case "zoom_in":
applyAppZoom((window.__bdsAppZoom || 1) + 0.1);
return true;
case "zoom_out":
applyAppZoom((window.__bdsAppZoom || 1) - 0.1);
return true;
case "toggle_full_screen":
if (document.fullscreenElement) {
document.exitFullscreen?.();
} else {
document.documentElement.requestFullscreen?.();
}
return true;
default:
return false;
}
};
// js/hooks/app_shell.js
var AppShell = {
mounted() { mounted() {
this.shortcuts = parseShortcutConfig(this.el.dataset.shortcuts); this.shortcuts = parseShortcutConfig(this.el.dataset.shortcuts);
this.currentProjectId = this.el.dataset.projectId || ""; this.currentProjectId = this.el.dataset.projectId || "";
@@ -9026,7 +9038,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
}; };
this.handleEvent("menu-runtime-command", ({ action }) => { this.handleEvent("menu-runtime-command", ({ action }) => {
if (action) { if (action) {
menuRuntimeCommandRunner(String(action)); runMenuRuntimeCommand(String(action));
} }
}); });
window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction); window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction);
@@ -9071,8 +9083,10 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.pushEvent("sync_ui_language", { language: stored }); this.pushEvent("sync_ui_language", { language: stored });
} }
} }
}, };
SidebarInteractions: {
// js/hooks/sidebar_interactions.js
var SidebarInteractions = {
mounted() { mounted() {
this.handleDblClick = (event) => { this.handleDblClick = (event) => {
const button = event.target.closest("[data-testid='sidebar-open-item']"); const button = event.target.closest("[data-testid='sidebar-open-item']");
@@ -9091,8 +9105,10 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
destroyed() { destroyed() {
this.el.removeEventListener("dblclick", this.handleDblClick); this.el.removeEventListener("dblclick", this.handleDblClick);
} }
}, };
SettingsSectionScroll: {
// js/hooks/section_scroll.js
var makeSectionScrollHook = (datasetKey) => ({
mounted() { mounted() {
this.lastTargetId = null; this.lastTargetId = null;
this.scrollToSelectedSection(); this.scrollToSelectedSection();
@@ -9101,7 +9117,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.scrollToSelectedSection(); this.scrollToSelectedSection();
}, },
scrollToSelectedSection() { scrollToSelectedSection() {
const targetId = this.el.dataset.settingsScrollTarget; const targetId = this.el.dataset[datasetKey];
if (!targetId || targetId === this.lastTargetId) { if (!targetId || targetId === this.lastTargetId) {
return; return;
} }
@@ -9113,30 +9129,12 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
} }
}); });
} }
},
TagsSectionScroll: {
mounted() {
this.lastTargetId = null;
this.scrollToSelectedSection();
},
updated() {
this.scrollToSelectedSection();
},
scrollToSelectedSection() {
const targetId = this.el.dataset.tagsScrollTarget;
if (!targetId || targetId === this.lastTargetId) {
return;
}
this.lastTargetId = targetId;
window.requestAnimationFrame(() => {
const target = document.getElementById(targetId);
if (target && this.el.contains(target)) {
target.scrollIntoView({ block: "start", behavior: "smooth" });
}
}); });
} var SettingsSectionScroll = makeSectionScrollHook("settingsScrollTarget");
}, var TagsSectionScroll = makeSectionScrollHook("tagsScrollTarget");
ChatSurface: {
// js/hooks/chat_surface.js
var ChatSurface = {
mounted() { mounted() {
this.stickToBottom = true; this.stickToBottom = true;
this.scrollContainer = null; this.scrollContainer = null;
@@ -9240,8 +9238,10 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.scrollContainer.removeEventListener("scroll", this.handleScroll); this.scrollContainer.removeEventListener("scroll", this.handleScroll);
} }
} }
}, };
MenuEditorTree: {
// js/hooks/menu_editor_tree.js
var MenuEditorTree = {
mounted() { mounted() {
this.dragItemId = null; this.dragItemId = null;
this.dragSourceEl = null; this.dragSourceEl = null;
@@ -9341,8 +9341,10 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.el.removeEventListener("dragleave", this.handleDragLeave); this.el.removeEventListener("dragleave", this.handleDragLeave);
this.el.removeEventListener("dragend", this.handleDragEnd); this.el.removeEventListener("dragend", this.handleDragEnd);
} }
}, };
MonacoEditor: {
// js/hooks/monaco_editor.js
var MonacoEditor = {
mounted() { mounted() {
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea"); this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");
this.host = this.el.querySelector(".monaco-editor-instance"); this.host = this.el.querySelector(".monaco-editor-instance");
@@ -9458,12 +9460,12 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
]); ]);
this.editor.focus(); this.editor.focus();
}; };
monacoServices.loadMonaco().then(async (monaco) => { loadMonaco().then(async (monaco) => {
if (!this.host || !this.textarea) { if (!this.host || !this.textarea) {
return; return;
} }
await this.waitForMonacoVisibleSize(); await this.waitForMonacoVisibleSize();
monacoServices.ensureMonacoTheme(monaco); ensureMonacoTheme(monaco);
this.editor = monaco.editor.create(this.host, { this.editor = monaco.editor.create(this.host, {
value: this.textarea.value || "", value: this.textarea.value || "",
language: this.language, language: this.language,
@@ -9486,7 +9488,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
tabSize: 2, tabSize: 2,
insertSpaces: true insertSpaces: true
}); });
monacoEditors.set(this.editorId || this.el.id, this.editor); registerMonacoEditor(this.editorId || this.el.id, this.editor);
monaco.editor.setTheme("bds-theme"); monaco.editor.setTheme("bds-theme");
this.syncEditorFromTextarea(); this.syncEditorFromTextarea();
this.layoutEditorSoon(); this.layoutEditorSoon();
@@ -9511,8 +9513,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
if (!this.editor || !this.textarea) { if (!this.editor || !this.textarea) {
return; return;
} }
monacoServices.loadMonaco().then((monaco) => { loadMonaco().then((monaco) => {
monacoServices.ensureMonacoTheme(monaco); ensureMonacoTheme(monaco);
monaco.editor.setTheme("bds-theme"); monaco.editor.setTheme("bds-theme");
if (this.editor.getModel()?.getLanguageId() !== this.language) { if (this.editor.getModel()?.getLanguageId() !== this.language) {
monaco.editor.setModelLanguage(this.editor.getModel(), this.language); monaco.editor.setModelLanguage(this.editor.getModel(), this.language);
@@ -9526,11 +9528,13 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
window.clearTimeout(this.syncTimer); window.clearTimeout(this.syncTimer);
this.visibleSizeObserver?.disconnect(); this.visibleSizeObserver?.disconnect();
this.changeSubscription?.dispose(); this.changeSubscription?.dispose();
monacoEditors.delete(this.editorId || this.el.id); unregisterMonacoEditor(this.editorId || this.el.id);
this.editor?.dispose(); this.editor?.dispose();
} }
}, };
MonacoDiffEditor: {
// js/hooks/monaco_diff_editor.js
var MonacoDiffEditor = {
mounted() { mounted() {
this.host = this.el.querySelector(".monaco-diff-editor-instance"); this.host = this.el.querySelector(".monaco-diff-editor-instance");
this.originalInput = this.el.querySelector(".monaco-diff-original"); this.originalInput = this.el.querySelector(".monaco-diff-original");
@@ -9568,11 +9572,11 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.editor.setModel({ original: this.originalModel, modified: this.modifiedModel }); this.editor.setModel({ original: this.originalModel, modified: this.modifiedModel });
this.lastFilePath = this.filePath; this.lastFilePath = this.filePath;
}; };
monacoServices.loadMonaco().then((monaco) => { loadMonaco().then((monaco) => {
if (!this.host) { if (!this.host) {
return; return;
} }
monacoServices.ensureMonacoTheme(monaco); ensureMonacoTheme(monaco);
this.editor = monaco.editor.createDiffEditor(this.host, { this.editor = monaco.editor.createDiffEditor(this.host, {
theme: "bds-theme", theme: "bds-theme",
automaticLayout: true, automaticLayout: true,
@@ -9600,8 +9604,8 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
if (!this.editor) { if (!this.editor) {
return; return;
} }
monacoServices.loadMonaco().then((monaco) => { loadMonaco().then((monaco) => {
monacoServices.ensureMonacoTheme(monaco); ensureMonacoTheme(monaco);
monaco.editor.setTheme("bds-theme"); monaco.editor.setTheme("bds-theme");
this.editor.updateOptions({ this.editor.updateOptions({
renderSideBySide: this.viewStyle === "side-by-side", renderSideBySide: this.viewStyle === "side-by-side",
@@ -9632,11 +9636,26 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"
this.modifiedModel?.dispose(); this.modifiedModel?.dispose();
this.editor?.dispose(); this.editor?.dispose();
} }
}
}; };
// js/hooks/index.js
var Hooks2 = {
AppShell,
SidebarInteractions,
SettingsSectionScroll,
TagsSectionScroll,
ChatSurface,
MenuEditorTree,
MonacoEditor,
MonacoDiffEditor
};
// js/app.js
document.addEventListener("DOMContentLoaded", () => {
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const liveSocket = new LiveSocket2("/live", Socket, { const liveSocket = new LiveSocket2("/live", Socket, {
params: { _csrf_token: csrfToken }, params: { _csrf_token: csrfToken },
hooks: createHooks(Hooks2), hooks: Hooks2,
metadata: { metadata: {
keydown: (event) => ({ keydown: (event) => ({
key: event.key, key: event.key,

View File

@@ -4005,7 +4005,7 @@ defmodule BDS.Desktop.ShellLiveTest do
end end
test "chat editor hook reopens server-expanded A2UI surfaces after patches" do test "chat editor hook reopens server-expanded A2UI surfaces after patches" do
live_js = File.read!(Path.expand("../../../assets/js/app.js", __DIR__)) live_js = File.read!(Path.expand("../../../assets/js/hooks/chat_surface.js", __DIR__))
chat_editor = chat_editor =
File.read!(Path.expand("../../../lib/bds/desktop/shell_live/chat_editor.ex", __DIR__)) File.read!(Path.expand("../../../lib/bds/desktop/shell_live/chat_editor.ex", __DIR__))
@@ -4161,7 +4161,7 @@ defmodule BDS.Desktop.ShellLiveTest do
assert css =~ "max-height: 22px;" assert css =~ "max-height: 22px;"
assert css =~ "padding: 0;" assert css =~ "padding: 0;"
live_js = File.read!(Path.expand("../../../assets/js/app.js", __DIR__)) live_js = File.read!(Path.expand("../../../assets/js/hooks/chat_surface.js", __DIR__))
assert live_js =~ assert live_js =~
"minHeight = parseFloat(styles.getPropertyValue(\"--chat-input-min-height\"))" "minHeight = parseFloat(styles.getPropertyValue(\"--chat-input-min-height\"))"

View File

@@ -28,6 +28,13 @@ defmodule BDS.UI.ShellTest do
|> Enum.join("\n") |> Enum.join("\n")
end end
defp live_js_source do
Path.wildcard("/Users/gb/Projects/bDS2/assets/js/**/*.js")
|> Enum.sort()
|> Enum.map(&File.read!/1)
|> Enum.join("\n")
end
test "registry exposes the shared sidebar and editor contracts for the base shell" do test "registry exposes the shared sidebar and editor contracts for the base shell" do
sidebar_views = Registry.sidebar_views() sidebar_views = Registry.sidebar_views()
editor_routes = Registry.editor_routes() editor_routes = Registry.editor_routes()
@@ -124,7 +131,7 @@ defmodule BDS.UI.ShellTest do
test "desktop shell keeps the compact frame metrics and live bootstrap assets" do test "desktop shell keeps the compact frame metrics and live bootstrap assets" do
css = css_source() css = css_source()
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
assert File.exists?("/Users/gb/Projects/bDS2/assets/css/shell.css") assert File.exists?("/Users/gb/Projects/bDS2/assets/css/shell.css")
@@ -255,7 +262,7 @@ defmodule BDS.UI.ShellTest do
test "phase 5 desktop-specific surfaces stay in source modules with responsive behavior" do test "phase 5 desktop-specific surfaces stay in source modules with responsive behavior" do
css = css_source() css = css_source()
app_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") app_js = live_js_source()
assert css =~ ".ai-suggestions-modal-backdrop" assert css =~ ".ai-suggestions-modal-backdrop"
assert css =~ ".gallery-overlay" assert css =~ ".gallery-overlay"
@@ -304,15 +311,28 @@ defmodule BDS.UI.ShellTest do
test "live javascript is split into focused Phoenix asset modules" do test "live javascript is split into focused Phoenix asset modules" do
app_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") app_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js")
hooks_index = File.read!("/Users/gb/Projects/bDS2/assets/js/hooks/index.js")
assert app_js =~ ~s(import { createHooks } from "./hooks/index.js";) assert app_js =~ ~s(import { Hooks } from "./hooks/index.js";)
assert app_js =~ ~s(import { syncTitlebarOverlayInsets } from "./bridges/titlebar_overlay.js";) assert hooks_index =~ ~s(import { AppShell } from "./app_shell.js";)
assert app_js =~ ~s(import { createMenuRuntimeCommandRunner } from "./bridges/menu_runtime.js";) assert hooks_index =~ ~s(import { ChatSurface } from "./chat_surface.js";)
assert app_js =~ ~s(import { createMonacoServices } from "./monaco/services.js";) assert hooks_index =~ ~s(import { MenuEditorTree } from "./menu_editor_tree.js";)
assert hooks_index =~ ~s(import { MonacoEditor } from "./monaco_editor.js";)
assert hooks_index =~ ~s(import { MonacoDiffEditor } from "./monaco_diff_editor.js";)
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/index.js") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/index.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/app_shell.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/chat_surface.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/menu_editor_tree.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/monaco_editor.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/monaco_diff_editor.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/sidebar_interactions.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/section_scroll.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/menu_runtime.js") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/menu_runtime.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/document_commands.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/monaco/services.js") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/monaco/services.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/monaco/theme.js")
assert File.exists?("/Users/gb/Projects/bDS2/assets/js/monaco/languages.js")
end end
test "top level shell render uses utility classes for common layout" do test "top level shell render uses utility classes for common layout" do
@@ -368,7 +388,7 @@ defmodule BDS.UI.ShellTest do
test "monaco editor styling forces the internal editor surface to the dark theme" do test "monaco editor styling forces the internal editor surface to the dark theme" do
css = css_source() css = css_source()
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
assert css =~ ".monaco-editor .margin" assert css =~ ".monaco-editor .margin"
assert css =~ ".monaco-editor-background" assert css =~ ".monaco-editor-background"
@@ -379,7 +399,7 @@ defmodule BDS.UI.ShellTest do
end end
test "monaco editor hook forces first visible layout and textarea content sync" do test "monaco editor hook forces first visible layout and textarea content sync" do
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
assert live_js =~ "this.syncEditorFromTextarea" assert live_js =~ "this.syncEditorFromTextarea"
assert live_js =~ "this.layoutEditorSoon" assert live_js =~ "this.layoutEditorSoon"
@@ -391,7 +411,7 @@ defmodule BDS.UI.ShellTest do
end end
test "monaco theme uses normalized app colors before defining the dark theme" do test "monaco theme uses normalized app colors before defining the dark theme" do
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
assert live_js =~ "normalizeMonacoColor" assert live_js =~ "normalizeMonacoColor"
assert live_js =~ "base: \"vs-dark\"" assert live_js =~ "base: \"vs-dark\""
@@ -400,7 +420,7 @@ defmodule BDS.UI.ShellTest do
end end
test "desktop shell assets persist workbench layout per project" do test "desktop shell assets persist workbench layout per project" do
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex") live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex")
session_util_ex = session_util_ex =
@@ -416,7 +436,7 @@ defmodule BDS.UI.ShellTest do
test "desktop shell assets reveal loaded media sidebar thumbnails" do test "desktop shell assets reveal loaded media sidebar thumbnails" do
css = css_source() css = css_source()
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
assert css =~ ".media-thumbnail.is-loaded .media-thumbnail-image" assert css =~ ".media-thumbnail.is-loaded .media-thumbnail-image"
assert css =~ ".media-thumbnail.is-loaded .media-thumbnail-fallback" assert css =~ ".media-thumbnail.is-loaded .media-thumbnail-fallback"
@@ -444,7 +464,7 @@ defmodule BDS.UI.ShellTest do
test "desktop shell assets keep old activity, tab, focus, and titlebar overlay parity rules" do test "desktop shell assets keep old activity, tab, focus, and titlebar overlay parity rules" do
css = css_source() css = css_source()
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
titlebar_js = File.read!("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js") titlebar_js = File.read!("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
@@ -494,7 +514,7 @@ defmodule BDS.UI.ShellTest do
test "desktop shell assets keep legacy titlebar menu keyboard and anchoring behavior" do test "desktop shell assets keep legacy titlebar menu keyboard and anchoring behavior" do
css = css_source() css = css_source()
live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_js = live_js_source()
live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex") live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")