Files
SiteCompanion/sitecompanion/settings.js
Peisong Xiao f0db7bb74a v0.4.8-dev New Release (#3)
# New Features
- Added custom prompt mode
- Always use the default environment and profile for a more compact UI
- Added option to hide the toolbar when it's empty
- Added documentation and icon

# Fixed bugs
- Fixed issue with config returning to defaults
- Fixed TOC lag when cards update
- Fixed some UI consistency issues
- Dynamically show site text char count in popup UI

Reviewed-on: #3
2026-01-20 05:41:07 +00:00

6950 lines
226 KiB
JavaScript

const saveBtnSidebar = document.getElementById("saveBtnSidebar");
const addApiConfigBtn = document.getElementById("addApiConfigBtn");
const apiConfigsContainer = document.getElementById("apiConfigs");
const addApiKeyBtn = document.getElementById("addApiKeyBtn");
const apiKeysContainer = document.getElementById("apiKeys");
const addEnvConfigBtn = document.getElementById("addEnvConfigBtn");
const envConfigsContainer = document.getElementById("envConfigs");
const addTaskBtn = document.getElementById("addTaskBtn");
const tasksContainer = document.getElementById("tasks");
const addProfileBtn = document.getElementById("addProfileBtn");
const profilesContainer = document.getElementById("profiles");
const addWorkspaceBtn = document.getElementById("addWorkspaceBtn");
const workspacesContainer = document.getElementById("workspaces");
const addSiteBtn = document.getElementById("addSiteBtn");
const sitesContainer = document.getElementById("sites");
const addShortcutBtn = document.getElementById("addShortcutBtn");
const shortcutsContainer = document.getElementById("shortcuts");
const statusSidebarEl = document.getElementById("statusSidebar");
const sidebarErrorsEl = document.getElementById("sidebarErrors");
const themeSelect = document.getElementById("themeSelect");
const toolbarPositionSelect = document.getElementById("toolbarPositionSelect");
const emptyToolbarBehaviorSelect = document.getElementById(
"emptyToolbarBehaviorSelect"
);
const toolbarAutoHide = document.getElementById("toolbarAutoHide");
const alwaysShowOutput = document.getElementById("alwaysShowOutput");
const alwaysUseDefaultEnvProfileSelect = document.getElementById(
"alwaysUseDefaultEnvProfileSelect"
);
const globalSitesContainer = document.getElementById("globalSites");
const toc = document.querySelector(".toc");
const tocResizer = document.getElementById("tocResizer");
const settingsLayout = document.querySelector(".settings-layout");
let initialSiteIds = new Set();
let lastSavedSnapshot = "";
let hasUnsavedChanges = false;
let dirtyCheckFrame = null;
let statusClearTimer = null;
let suppressDirtyTracking = true;
let dirtyObserver = null;
let tocTargets = [];
let tocHighlightFrame = null;
let activeTocLink = null;
let tocTargetMap = new WeakMap();
const OPENAI_DEFAULTS = {
apiBaseUrl: "https://api.openai.com/v1"
};
const DEFAULT_MODEL = "gpt-5.2";
const DEFAULT_SYSTEM_PROMPT = "";
const SIDEBAR_WIDTH_KEY = "sidebarWidth";
function isPlainObject(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function escapeSelector(value) {
if (window.CSS && typeof CSS.escape === "function") {
return CSS.escape(value);
}
return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
}
function buildClassSelector(className) {
const parts = String(className || "")
.trim()
.split(/\s+/)
.filter(Boolean);
if (!parts.length) return "";
return parts.map((name) => `.${escapeSelector(name)}`).join("");
}
function parseLegacyDomSelectorString(rawValue) {
const trimmed = String(rawValue || "").trim();
if (!trimmed) return null;
const classMatch = trimmed.match(
/^(?:document\.)?getElementsByClassName\(\s*(['"])(.+?)\1\s*\)\s*\[\s*(\d+)\s*\]\s*(?:\.innerText\s*)?;?$/i
);
if (classMatch) {
const selector = buildClassSelector(classMatch[2]);
if (!selector) {
return { target: null, error: "Missing extraction target." };
}
const index = Number.parseInt(classMatch[3], 10);
if (!Number.isInteger(index) || index < 0) {
return { target: null, error: "Invalid index." };
}
return { target: { kind: "cssAll", selector, index }, error: null };
}
if (trimmed.includes("getElementsByClassName")) {
return { target: null, error: "Unsupported extraction target." };
}
return null;
}
function parseLooseJsonInput(rawValue) {
const trimmed = String(rawValue || "").trim();
if (!trimmed.startsWith("{")) return null;
let normalized = trimmed;
normalized = normalized.replace(
/([{,]\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*:)/g,
'$1"$2"$3'
);
normalized = normalized.replace(
/'([^'\\]*(?:\\.[^'\\]*)*)'/g,
(_match, value) => `"${value.replace(/"/g, '\\"')}"`
);
return normalized;
}
function normalizeExtractionTargetValue(value) {
if (typeof value === "string") {
const legacy = parseLegacyDomSelectorString(value);
if (legacy) {
return legacy.target;
}
const trimmed = value.trim();
return trimmed ? { kind: "css", selector: trimmed } : null;
}
if (isPlainObject(value) && typeof value.kind === "string") {
return value;
}
return null;
}
function serializeExtractionTarget(target) {
if (!target) return "";
if (typeof target === "string") {
const legacy = parseLegacyDomSelectorString(target);
if (legacy?.target) return JSON.stringify(legacy.target);
const trimmed = target.trim();
if (!trimmed) return "";
return JSON.stringify({ kind: "css", selector: trimmed });
}
if (isPlainObject(target) && typeof target.kind === "string") {
return JSON.stringify(target);
}
return "";
}
function validateExtractionTarget(target) {
if (!target || typeof target !== "object") {
return "Missing extraction target.";
}
if (target.kind === "xpath") {
return "XPath not supported.";
}
if (target.kind === "css") {
return typeof target.selector === "string" && target.selector.trim()
? null
: "Missing extraction target.";
}
if (target.kind === "cssAll") {
if (typeof target.selector !== "string" || !target.selector.trim()) {
return "Missing extraction target.";
}
if (!Number.isInteger(target.index) || target.index < 0) {
return "Invalid index.";
}
return null;
}
if (target.kind === "textScope") {
return typeof target.text === "string" && target.text.trim()
? null
: "Missing extraction target.";
}
if (target.kind === "anchoredCss") {
const anchor = target.anchor;
if (!anchor || anchor.kind !== "textScope") {
return "Invalid anchor target.";
}
if (typeof anchor.text !== "string" || !anchor.text.trim()) {
return "Missing extraction target.";
}
if (typeof target.selector !== "string" || !target.selector.trim()) {
return "Missing extraction target.";
}
return null;
}
return "Unsupported extraction target.";
}
function parseExtractionTargetInput(rawValue) {
const trimmed = (rawValue || "").trim();
if (!trimmed) {
return { target: null, error: "Missing extraction target." };
}
const legacy = parseLegacyDomSelectorString(trimmed);
if (legacy) {
if (legacy.error) {
return { target: null, error: legacy.error };
}
const error = validateExtractionTarget(legacy.target);
return { target: legacy.target, error };
}
if (trimmed.startsWith("textScope:")) {
const text = trimmed.slice("textScope:".length).trim();
const target = { kind: "textScope", text };
const error = validateExtractionTarget(target);
return { target, error };
}
let target = null;
if (trimmed.startsWith("{")) {
try {
const parsed = JSON.parse(trimmed);
target = normalizeExtractionTargetValue(parsed);
} catch {
const normalized = parseLooseJsonInput(trimmed);
if (!normalized) {
return { target: null, error: "Invalid extraction target JSON." };
}
try {
const parsed = JSON.parse(normalized);
target = normalizeExtractionTargetValue(parsed);
} catch {
return { target: null, error: "Invalid extraction target JSON." };
}
}
} else {
target = { kind: "css", selector: trimmed };
}
if (!target) {
return { target: null, error: "Invalid extraction target." };
}
const error = validateExtractionTarget(target);
return { target, error };
}
function normalizeStoredExtractionTarget(site) {
const normalized = normalizeExtractionTargetValue(site?.extractTarget);
if (normalized) {
const changed = typeof site?.extractTarget === "string";
return { target: normalized, changed };
}
if (typeof site?.extractSelector === "string" && site.extractSelector.trim()) {
const legacy = parseLegacyDomSelectorString(site.extractSelector);
if (legacy?.target) {
return { target: legacy.target, changed: true };
}
return {
target: { kind: "css", selector: site.extractSelector.trim() },
changed: true
};
}
return { target: null, changed: false };
}
function getSidebarWidthLimits() {
const min = 160;
const max = Math.max(min, Math.min(360, window.innerWidth - 240));
return { min, max };
}
function applySidebarWidth(width) {
if (!toc) return;
const { min, max } = getSidebarWidthLimits();
if (!Number.isFinite(width)) {
toc.style.width = "";
toc.style.flex = "";
return;
}
const clamped = Math.min(Math.max(width, min), max);
toc.style.width = `${clamped}px`;
toc.style.flex = `0 0 ${clamped}px`;
}
function initSidebarResize() {
if (!toc || !tocResizer) return;
let startX = 0;
let startWidth = 0;
const onMouseMove = (event) => {
const delta = event.clientX - startX;
applySidebarWidth(startWidth + delta);
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
document.body.classList.remove("is-resizing");
const width = toc.getBoundingClientRect().width;
void chrome.storage.local.set({ [SIDEBAR_WIDTH_KEY]: Math.round(width) });
};
tocResizer.addEventListener("mousedown", (event) => {
event.preventDefault();
startX = event.clientX;
startWidth = toc.getBoundingClientRect().width;
document.body.classList.add("is-resizing");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
}
function getStorage(keys) {
return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
}
const SETTINGS_VIEW_STATE_KEY = "settingsViewState";
let settingsViewState = { open: {}, scrollTop: 0 };
let settingsViewStateTimer = null;
async function loadSettingsViewState() {
const stored = await getStorage([SETTINGS_VIEW_STATE_KEY]);
const state = stored[SETTINGS_VIEW_STATE_KEY];
if (!state || typeof state !== "object") return;
const open =
state.open && typeof state.open === "object" ? state.open : {};
settingsViewState = {
open,
scrollTop: Number.isFinite(state.scrollTop) ? state.scrollTop : 0
};
}
function scheduleSettingsViewStateSave() {
if (settingsViewStateTimer) clearTimeout(settingsViewStateTimer);
settingsViewStateTimer = setTimeout(() => {
settingsViewStateTimer = null;
void chrome.storage.local.set({
[SETTINGS_VIEW_STATE_KEY]: settingsViewState
});
}, 200);
}
function getDetailStateKey(details) {
return details?.dataset?.stateKey || details?.id || "";
}
function openDetails(details) {
if (!details) return;
details.open = true;
const key = getDetailStateKey(details);
if (!key) return;
settingsViewState.open[key] = true;
scheduleSettingsViewStateSave();
}
function centerCardInView(card) {
if (!card || typeof card.getBoundingClientRect !== "function") return;
requestAnimationFrame(() => {
if (!card.isConnected) return;
scrollCardToCenter(card);
});
}
function centerCardInViewAfterLayout(card, attempts = 4) {
if (!card || typeof card.getBoundingClientRect !== "function") return;
if (attempts <= 0) {
centerCardInView(card);
return;
}
requestAnimationFrame(() => {
if (!card.isConnected) return;
const rect = card.getBoundingClientRect();
if (rect.height <= 0 || rect.width <= 0) {
centerCardInViewAfterLayout(card, attempts - 1);
return;
}
centerCardInView(card);
});
}
function registerDetail(details, defaultOpen) {
if (!details || details.dataset.stateReady === "true") return;
const key = getDetailStateKey(details);
if (!key) return;
details.dataset.stateReady = "true";
const storedOpen = settingsViewState.open?.[key];
if (typeof storedOpen === "boolean") {
details.open = storedOpen;
} else if (typeof defaultOpen === "boolean") {
details.open = defaultOpen;
}
details.addEventListener("toggle", () => {
settingsViewState.open[key] = details.open;
if (!details.open) {
collapseChildDetails(details);
}
scheduleSettingsViewStateSave();
scheduleTocHighlight();
});
}
function collapseChildDetails(parent) {
if (!parent) return;
const children = parent.querySelectorAll("details");
children.forEach((child) => {
if (child.open) {
child.open = false;
const childKey = getDetailStateKey(child);
if (childKey) {
settingsViewState.open[childKey] = false;
}
}
});
}
function registerAllDetails() {
const detailsList = document.querySelectorAll("details");
detailsList.forEach((details) => {
if (!details.dataset.stateKey && details.id) {
details.dataset.stateKey = details.id;
}
registerDetail(details, details.open);
});
}
function restoreScrollPosition() {
if (!Number.isFinite(settingsViewState.scrollTop)) return;
requestAnimationFrame(() => {
window.scrollTo(0, settingsViewState.scrollTop || 0);
});
}
function handleSettingsScroll() {
settingsViewState.scrollTop = window.scrollY || 0;
scheduleSettingsViewStateSave();
scheduleTocHighlight();
}
function setStatus(message, options = {}) {
if (!statusSidebarEl) return;
const { tone = "normal", persist = false, restoreDirty = true } = options;
if (statusClearTimer) {
clearTimeout(statusClearTimer);
statusClearTimer = null;
}
statusSidebarEl.textContent = message;
statusSidebarEl.classList.toggle("is-dirty", tone === "dirty");
if (!message) return;
if (persist) return;
statusClearTimer = window.setTimeout(() => {
statusClearTimer = null;
if (statusSidebarEl?.textContent !== message) return;
if (hasUnsavedChanges && restoreDirty) {
setStatus("Unsaved changes.", {
tone: "dirty",
persist: true,
restoreDirty: false
});
return;
}
statusSidebarEl.textContent = "";
statusSidebarEl.classList.remove("is-dirty");
}, 2000);
}
function buildSettingsSnapshot() {
return JSON.stringify({
apiKeys: collectApiKeys(),
apiConfigs: collectApiConfigs(),
envConfigs: collectEnvConfigs(),
profiles: collectProfiles(),
tasks: collectTasks(),
shortcuts: collectShortcuts(),
workspaces: collectWorkspaces(),
sites: collectSites(),
theme: themeSelect?.value || "system",
toolbarPosition: toolbarPositionSelect
? toolbarPositionSelect.value
: "bottom-right",
emptyToolbarBehavior: emptyToolbarBehaviorSelect
? emptyToolbarBehaviorSelect.value
: "open",
toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true,
alwaysShowOutput: alwaysShowOutput ? alwaysShowOutput.checked : false,
alwaysUseDefaultEnvProfile: alwaysUseDefaultEnvProfileSelect
? alwaysUseDefaultEnvProfileSelect.value === "enabled"
: false
});
}
function setDirtyState(isDirty) {
if (hasUnsavedChanges === isDirty) return;
hasUnsavedChanges = isDirty;
if (hasUnsavedChanges) {
setStatus("Unsaved changes.", {
tone: "dirty",
persist: true,
restoreDirty: false
});
return;
}
if (statusSidebarEl?.classList.contains("is-dirty")) {
setStatus("");
}
}
function updateDirtyState() {
if (!lastSavedSnapshot) {
setDirtyState(false);
return;
}
const snapshot = buildSettingsSnapshot();
setDirtyState(snapshot !== lastSavedSnapshot);
}
function scheduleDirtyCheck() {
if (suppressDirtyTracking) return;
if (dirtyCheckFrame) return;
dirtyCheckFrame = requestAnimationFrame(() => {
dirtyCheckFrame = null;
updateDirtyState();
});
}
function captureSavedSnapshot() {
lastSavedSnapshot = buildSettingsSnapshot();
setDirtyState(false);
}
function initDirtyObserver() {
if (!settingsLayout || dirtyObserver) return;
dirtyObserver = new MutationObserver((mutations) => {
if (suppressDirtyTracking) return;
const hasChildChange = mutations.some(
(mutation) => mutation.type === "childList"
);
if (hasChildChange) scheduleDirtyCheck();
});
dirtyObserver.observe(settingsLayout, { childList: true, subtree: true });
}
let sidebarErrorFrame = null;
function scheduleSidebarErrors() {
if (!sidebarErrorsEl) return;
if (sidebarErrorFrame) return;
sidebarErrorFrame = requestAnimationFrame(() => {
sidebarErrorFrame = null;
updateSidebarErrors();
});
}
let tocUpdateFrame = null;
function scheduleTocUpdate() {
if (!toc) return;
if (tocUpdateFrame) return;
tocUpdateFrame = requestAnimationFrame(() => {
tocUpdateFrame = null;
updateToc(collectWorkspaces(), collectSites());
});
}
const TOC_NAME_INPUT_SELECTOR = [
".api-key-name",
".api-config-name",
".env-config-name",
".profile-name",
".task-name",
".shortcut-name",
".workspace-name",
".site-name",
".site-pattern"
].join(", ");
function handleTocNameInput(event) {
const target = event.target;
if (!(target instanceof Element)) return;
if (!target.matches(TOC_NAME_INPUT_SELECTOR)) return;
scheduleTocUpdate();
}
function renderGlobalSitesList(sites) {
if (!globalSitesContainer) return;
globalSitesContainer.innerHTML = "";
const globalSites = (sites || []).filter(
(site) => (site.workspaceId || "global") === "global"
);
if (!globalSites.length) {
const empty = document.createElement("div");
empty.textContent = "No sites inherit from global.";
empty.className = "hint";
globalSitesContainer.appendChild(empty);
return;
}
for (const site of globalSites) {
const link = document.createElement("a");
link.href = "#";
link.textContent = site.name || site.urlPattern || "Untitled Site";
link.addEventListener("click", (e) => {
e.preventDefault();
const card = document.querySelector(`.site-card[data-id="${site.id}"]`);
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
openDetailsChain(document.getElementById("sites-panel"));
}
});
globalSitesContainer.appendChild(link);
}
}
function renderWorkspaceSitesList(list, workspaceId, sites) {
if (!list) return;
const ownedSites = (sites || []).filter(
(site) => (site.workspaceId || "global") === workspaceId
);
list.innerHTML = "";
if (!ownedSites.length) {
const empty = document.createElement("div");
empty.className = "hint";
empty.textContent = "No sites inherit from this workspace.";
list.appendChild(empty);
return;
}
for (const site of ownedSites) {
const link = document.createElement("a");
link.href = "#";
link.textContent = site.name || site.urlPattern || "Untitled Site";
link.addEventListener("click", (e) => {
e.preventDefault();
const siteCard = document.querySelector(
`.site-card[data-id="${site.id}"]`
);
if (siteCard) {
siteCard.scrollIntoView({ behavior: "smooth", block: "center" });
openDetailsChain(document.getElementById("sites-panel"));
}
});
list.appendChild(link);
}
}
function applyTheme(theme) {
const value = theme || "system";
document.documentElement.dataset.theme = value;
}
function newTaskId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `task-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newApiKeyId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newApiConfigId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `config-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newEnvConfigId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `env-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newProfileId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `profile-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newWorkspaceId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `ws-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newSiteId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `site-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function newShortcutId() {
if (crypto?.randomUUID) return crypto.randomUUID();
return `shortcut-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
}
function buildChatUrlFromBase(baseUrl) {
const trimmed = (baseUrl || "").trim().replace(/\/+$/, "");
if (!trimmed) return "";
if (trimmed.endsWith("/chat/completions")) return trimmed;
return `${trimmed}/chat/completions`;
}
function collectNames(container, selector) {
if (!container) return [];
return [...container.querySelectorAll(selector)]
.map((input) => (input.value || "").trim())
.filter(Boolean);
}
function buildUniqueDefaultName(names) {
const lower = new Set(names.map((name) => name.toLowerCase()));
if (!lower.has("default")) return "Default";
let index = 2;
while (lower.has(`default-${index}`)) {
index += 1;
}
return `Default-${index}`;
}
function buildUniqueNumberedName(prefix, names) {
const base = (prefix || "").trim() || "New";
const lower = new Set(names.map((name) => name.toLowerCase()));
let index = 1;
let candidate = `${base}-${index}`;
while (lower.has(candidate.toLowerCase())) {
index += 1;
candidate = `${base}-${index}`;
}
return candidate;
}
function ensureUniqueName(desired, existingNames) {
const trimmed = (desired || "").trim();
const lowerNames = existingNames.map((name) => name.toLowerCase());
if (trimmed && !lowerNames.includes(trimmed.toLowerCase())) {
return trimmed;
}
return buildUniqueDefaultName(existingNames);
}
function isEnabled(value) {
return value !== false;
}
function populateSelect(select, items, emptyLabel) {
const preferred = select.dataset.preferred || select.value;
select.innerHTML = "";
if (!items.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = emptyLabel;
select.appendChild(option);
select.disabled = true;
return;
}
select.disabled = false;
for (const item of items) {
const option = document.createElement("option");
option.value = item.id;
option.textContent = item.name || "Default";
select.appendChild(option);
}
if (preferred && items.some((item) => item.id === preferred)) {
select.value = preferred;
} else {
select.value = items[0]?.id || "";
}
select.dataset.preferred = select.value;
}
function buildItemMap(items) {
const map = new Map();
(Array.isArray(items) ? items : []).forEach((item) => {
if (item?.id) map.set(item.id, item);
});
return map;
}
function mergeById(...lists) {
const map = new Map();
lists.flat().forEach((item) => {
if (item?.id) map.set(item.id, item);
});
return [...map.values()];
}
function setupCardPanel(card, nameInput, fallbackTitle, options = {}) {
const { subPanel = true } = options;
card.open = false;
card.classList.add("panel");
if (subPanel) {
card.classList.add("sub-panel");
}
card.classList.add("settings-card");
const summary = document.createElement("summary");
summary.className = "panel-summary card-summary";
const row = document.createElement("div");
row.className = "panel-summary-row";
const summaryLeft = document.createElement("div");
summaryLeft.className = "panel-summary-left";
const summaryRight = document.createElement("div");
summaryRight.className = "panel-summary-right";
const rowTitle = document.createElement("span");
rowTitle.className = "row-title";
const title = document.createElement("span");
title.className = "card-title";
rowTitle.appendChild(title);
summaryLeft.appendChild(rowTitle);
row.appendChild(summaryLeft);
row.appendChild(summaryRight);
summary.appendChild(row);
const body = document.createElement("div");
body.className = "panel-body card-body";
card.appendChild(summary);
card.appendChild(body);
summary.addEventListener("click", (event) => {
if (event.target.closest("button")) {
event.preventDefault();
}
});
const updateTitle = () => {
const text = (nameInput?.value || "").trim();
title.textContent = text || fallbackTitle;
};
updateTitle();
if (nameInput) {
nameInput.addEventListener("input", updateTitle);
}
registerDetail(card, false);
return { body, summaryLeft, summaryRight, updateTitle };
}
function buildEnabledToggleButton(enabledInput) {
const button = document.createElement("button");
button.type = "button";
button.className = "enabled-toggle ghost";
const updateState = () => {
const enabled = enabledInput.checked;
button.textContent = enabled ? "Enabled" : "Disabled";
button.classList.toggle("accent", enabled);
button.classList.toggle("ghost", !enabled);
button.setAttribute("aria-pressed", enabled ? "true" : "false");
};
button.addEventListener("click", (event) => {
event.preventDefault();
enabledInput.checked = !enabledInput.checked;
enabledInput.dispatchEvent(new Event("change", { bubbles: true }));
updateState();
});
enabledInput.addEventListener("change", updateState);
updateState();
return button;
}
function populateSelectPreserving(select, items, emptyLabel, allItemsById) {
const preferred = select.dataset.preferred || select.value;
populateSelect(select, items, emptyLabel);
if (!preferred) return;
const hasPreferred = [...select.options].some(
(option) => option.value === preferred
);
if (hasPreferred) return;
const fallback = allItemsById?.get(preferred);
if (!fallback) return;
const option = document.createElement("option");
option.value = preferred;
option.textContent = `${fallback.name || "Unavailable"} (disabled)`;
option.disabled = true;
select.appendChild(option);
select.value = preferred;
select.dataset.preferred = preferred;
}
function normalizeConfigList(list) {
return Array.isArray(list)
? list.map((item) => ({ ...item, enabled: item.enabled !== false }))
: [];
}
const TEMPLATE_PLACEHOLDERS = [
"SYSTEM_PROMPT_GOES_HERE",
"PROMPT_GOES_HERE",
"API_KEY_GOES_HERE",
"MODEL_GOES_HERE",
"API_BASE_URL_GOES_HERE"
].sort((a, b) => b.length - a.length);
function buildTemplateValidationSource(template) {
let output = template || "";
for (const token of TEMPLATE_PLACEHOLDERS) {
output = output.split(`\"${token}\"`).join(JSON.stringify("PLACEHOLDER"));
output = output.split(token).join("null");
}
return output;
}
function normalizeTemplateInput(template) {
return (template || "")
.replace(/\uFEFF/g, "")
.replace(/[\u200B-\u200D\u2060]/g, "")
.replace(/[\u2028\u2029]/g, "\n")
.replace(/[\u0000-\u001F]/g, (char) =>
char === "\n" || char === "\r" || char === "\t" ? char : " "
)
.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, " ");
}
function isValidTemplateJson(template) {
if (!template) return false;
const normalized = normalizeTemplateInput(template);
try {
JSON.parse(normalized);
return true;
} catch {
// Fall through to placeholder-neutralized parsing.
}
try {
JSON.parse(buildTemplateValidationSource(normalized));
return true;
} catch {
return false;
}
}
function normalizeDisabledInherited(source) {
const data = source && typeof source === "object" ? source : {};
return {
envs: Array.isArray(data.envs) ? data.envs : [],
profiles: Array.isArray(data.profiles) ? data.profiles : [],
tasks: Array.isArray(data.tasks) ? data.tasks : [],
shortcuts: Array.isArray(data.shortcuts) ? data.shortcuts : [],
apiConfigs: Array.isArray(data.apiConfigs) ? data.apiConfigs : []
};
}
function getTopEnvId() {
return collectEnvConfigs().find((env) => isEnabled(env.enabled))?.id || "";
}
function getTopProfileId() {
return collectProfiles().find((profile) => isEnabled(profile.enabled))?.id || "";
}
function setApiConfigAdvanced(card, isAdvanced) {
card.classList.toggle("is-advanced", isAdvanced);
card.dataset.mode = isAdvanced ? "advanced" : "basic";
const basicFields = card.querySelectorAll(
".basic-only input, .basic-only textarea"
);
const advancedFields = card.querySelectorAll(
".advanced-only input, .advanced-only textarea"
);
basicFields.forEach((field) => {
field.disabled = isAdvanced;
});
advancedFields.forEach((field) => {
field.disabled = !isAdvanced;
});
const resetBtn = card.querySelector(".reset-openai");
if (resetBtn) resetBtn.disabled = false;
const advancedBtn = card.querySelector(".advanced-toggle");
if (advancedBtn) {
advancedBtn.disabled = false;
advancedBtn.classList.toggle("hidden", isAdvanced);
}
}
function readApiConfigFromCard(card) {
const nameInput = card.querySelector(".api-config-name");
const keySelect = card.querySelector(".api-config-key-select");
const baseInput = card.querySelector(".api-config-base");
const modelInput = card.querySelector(".api-config-model");
const urlInput = card.querySelector(".api-config-url");
const templateInput = card.querySelector(".api-config-template");
const enabledInput = card.querySelector(".config-enabled");
const isAdvanced = card.classList.contains("is-advanced");
return {
id: card.dataset.id || newApiConfigId(),
name: (nameInput?.value || "Default").trim(),
apiKeyId: keySelect?.value || "",
apiBaseUrl: (baseInput?.value || "").trim(),
model: (modelInput?.value || "").trim(),
apiUrl: (urlInput?.value || "").trim(),
requestTemplate: (templateInput?.value || "").trim(),
advanced: isAdvanced,
enabled: enabledInput ? enabledInput.checked : true
};
}
function buildApiConfigCard(config) {
const card = document.createElement("details");
card.className = "api-config-card";
card.dataset.id = config.id || newApiConfigId();
card.dataset.stateKey = `api-config:${card.dataset.id}`;
const isAdvanced = Boolean(config.advanced);
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.className = "config-enabled";
enabledInput.checked = config.enabled !== false;
enabledInput.addEventListener("change", () => {
updateEnvApiOptions();
});
enabledInput.classList.add("hidden");
const nameLabel = document.createElement("label");
nameLabel.textContent = "Name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = config.name || "";
nameInput.className = "api-config-name";
const nameField = document.createElement("div");
nameField.className = "field";
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const { body, summaryLeft, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled"
);
const enabledToggle = buildEnabledToggleButton(enabledInput);
summaryLeft.prepend(enabledToggle);
summaryLeft.appendChild(enabledInput);
const keyField = document.createElement("div");
keyField.className = "field";
const keyLabel = document.createElement("label");
keyLabel.textContent = "API Key";
const keySelect = document.createElement("select");
keySelect.className = "api-config-key-select";
keySelect.dataset.preferred = config.apiKeyId || "";
keyField.appendChild(keyLabel);
keyField.appendChild(keySelect);
const baseField = document.createElement("div");
baseField.className = "field basic-only";
const baseLabel = document.createElement("label");
baseLabel.textContent = "API Base URL";
const baseInput = document.createElement("input");
baseInput.type = "text";
baseInput.placeholder = OPENAI_DEFAULTS.apiBaseUrl;
baseInput.value = config.apiBaseUrl || "";
baseInput.className = "api-config-base";
baseField.appendChild(baseLabel);
baseField.appendChild(baseInput);
const modelField = document.createElement("div");
modelField.className = "field basic-only api-config-model-field";
const modelLabel = document.createElement("label");
modelLabel.textContent = "Model name";
const modelInput = document.createElement("input");
modelInput.type = "text";
modelInput.placeholder = DEFAULT_MODEL;
modelInput.value = config.model || "";
modelInput.className = "api-config-model";
modelField.appendChild(modelLabel);
modelField.appendChild(modelInput);
const primaryRow = document.createElement("div");
primaryRow.className = "inline-fields api-config-primary";
primaryRow.appendChild(nameField);
primaryRow.appendChild(keyField);
primaryRow.appendChild(modelField);
const urlField = document.createElement("div");
urlField.className = "field advanced-only";
const urlLabel = document.createElement("label");
urlLabel.textContent = "API URL";
const urlInput = document.createElement("input");
urlInput.type = "text";
urlInput.placeholder = "https://api.example.com/v1/chat/completions";
urlInput.value = config.apiUrl || "";
urlInput.className = "api-config-url";
urlField.appendChild(urlLabel);
urlField.appendChild(urlInput);
const templateField = document.createElement("div");
templateField.className = "field advanced-only";
const templateLabel = document.createElement("label");
templateLabel.textContent = "Request JSON body";
const templateInput = document.createElement("textarea");
templateInput.rows = 8;
templateInput.placeholder = [
"{",
" \"stream\": true,",
" \"messages\": [",
" { \"role\": \"system\", \"content\": \"SYSTEM_PROMPT_GOES_HERE\" },",
" { \"role\": \"user\", \"content\": \"PROMPT_GOES_HERE\" }",
" ],",
" \"api_key\": \"API_KEY_GOES_HERE\"",
"}"
].join("\n");
templateInput.value = config.requestTemplate || "";
templateInput.className = "api-config-template";
templateField.appendChild(templateLabel);
templateField.appendChild(templateInput);
const actions = document.createElement("div");
actions.className = "api-config-actions";
const leftActions = document.createElement("div");
leftActions.className = "api-config-actions-left";
const rightActions = document.createElement("div");
rightActions.className = "api-config-actions-right";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
const duplicateBtn = document.createElement("button");
duplicateBtn.type = "button";
duplicateBtn.className = "ghost duplicate";
duplicateBtn.textContent = "Duplicate";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "accent add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => {
const first = apiConfigsContainer.firstElementChild;
if (!first || first === card) return;
animateCardMove(card, () => {
apiConfigsContainer.insertBefore(card, first);
}, { scrollToCenter: true });
updateApiConfigControls();
updateEnvApiOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
animateCardMove(card, () => {
apiConfigsContainer.insertBefore(card, previous);
});
updateApiConfigControls();
updateEnvApiOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
animateCardMove(card, () => {
apiConfigsContainer.insertBefore(card, next.nextElementSibling);
});
updateApiConfigControls();
updateEnvApiOptions();
});
duplicateBtn.addEventListener("click", () => {
const sourceRect = card.getBoundingClientRect();
const source = readApiConfigFromCard(card);
const names = collectNames(apiConfigsContainer, ".api-config-name");
const nameValue = source.name || "Default";
const copy = {
...source,
id: newApiConfigId(),
name: ensureUniqueName(`${nameValue} Copy`, names)
};
const newCard = buildApiConfigCard(copy);
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
animateDuplicateFromRect(newCard, sourceRect);
updateApiConfigKeyOptions();
updateEnvApiOptions();
updateApiConfigControls();
scheduleSidebarErrors();
centerCardInViewAfterLayout(newCard);
});
addBelowBtn.addEventListener("click", () => {
const name = buildUniqueNumberedName(
"New API",
collectNames(apiConfigsContainer, ".api-config-name")
);
const newCard = buildApiConfigCard({
id: newApiConfigId(),
name,
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
model: DEFAULT_MODEL,
apiUrl: "",
requestTemplate: "",
advanced: false
});
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
centerCardInView(newCard);
updateApiConfigKeyOptions();
updateEnvApiOptions();
updateApiConfigControls();
});
const advancedBtn = document.createElement("button");
advancedBtn.type = "button";
advancedBtn.className = "ghost advanced-toggle";
advancedBtn.textContent = "Advanced Mode";
advancedBtn.addEventListener("click", () => {
if (card.classList.contains("is-advanced")) return;
urlInput.value = buildChatUrlFromBase(baseInput.value);
templateInput.value = [
"{",
` \"model\": \"${modelInput.value || DEFAULT_MODEL}\",`,
" \"stream\": true,",
" \"messages\": [",
" { \"role\": \"system\", \"content\": \"SYSTEM_PROMPT_GOES_HERE\" },",
" { \"role\": \"user\", \"content\": \"PROMPT_GOES_HERE\" }",
" ],",
" \"api_key\": \"API_KEY_GOES_HERE\"",
"}"
].join("\n");
setApiConfigAdvanced(card, true);
updateEnvApiOptions();
});
const resetBtn = document.createElement("button");
resetBtn.type = "button";
resetBtn.className = "ghost reset-openai";
resetBtn.textContent = "Reset to OpenAI";
resetBtn.addEventListener("click", () => {
baseInput.value = OPENAI_DEFAULTS.apiBaseUrl;
modelInput.value = DEFAULT_MODEL;
urlInput.value = "";
templateInput.value = "";
setApiConfigAdvanced(card, false);
updateEnvApiOptions();
});
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateEnvApiOptions();
updateApiConfigControls();
});
const updateSelect = () => updateEnvApiOptions();
nameInput.addEventListener("input", updateSelect);
baseInput.addEventListener("input", updateSelect);
modelInput.addEventListener("input", updateSelect);
urlInput.addEventListener("input", updateSelect);
templateInput.addEventListener("input", updateSelect);
rightActions.appendChild(moveTopBtn);
rightActions.appendChild(moveUpBtn);
rightActions.appendChild(moveDownBtn);
rightActions.appendChild(duplicateBtn);
rightActions.appendChild(addBelowBtn);
rightActions.appendChild(deleteBtn);
leftActions.appendChild(advancedBtn);
leftActions.appendChild(resetBtn);
actions.appendChild(leftActions);
actions.appendChild(rightActions);
body.appendChild(primaryRow);
body.appendChild(baseField);
body.appendChild(urlField);
body.appendChild(templateField);
summaryRight.appendChild(actions);
setApiConfigAdvanced(card, isAdvanced);
return card;
}
function collectApiConfigs() {
const cards = [...apiConfigsContainer.querySelectorAll(".api-config-card")];
return cards.map((card) => readApiConfigFromCard(card));
}
function updateApiConfigControls() {
const cards = [...apiConfigsContainer.querySelectorAll(".api-config-card")];
cards.forEach((card, index) => {
const moveTopBtn = card.querySelector(".move-top");
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveTopBtn) moveTopBtn.disabled = index === 0;
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
scheduleSidebarErrors();
scheduleTocUpdate();
}
function buildApiKeyCard(entry) {
const card = document.createElement("details");
card.className = "api-key-card";
card.dataset.id = entry.id || newApiKeyId();
card.dataset.stateKey = `api-key:${card.dataset.id}`;
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.className = "config-enabled";
enabledInput.checked = entry.enabled !== false;
enabledInput.addEventListener("change", () => {
updateApiConfigKeyOptions();
});
enabledInput.classList.add("hidden");
const nameField = document.createElement("div");
nameField.className = "field";
const nameLabel = document.createElement("label");
nameLabel.textContent = "Name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = entry.name || "";
nameInput.className = "api-key-name";
const { body, summaryLeft, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled"
);
const enabledToggle = buildEnabledToggleButton(enabledInput);
summaryLeft.prepend(enabledToggle);
summaryLeft.appendChild(enabledInput);
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const keyField = document.createElement("div");
keyField.className = "field";
const keyLabel = document.createElement("label");
keyLabel.textContent = "Key";
const keyInline = document.createElement("div");
keyInline.className = "inline";
const keyInput = document.createElement("input");
keyInput.type = "password";
keyInput.autocomplete = "off";
keyInput.placeholder = "sk-...";
keyInput.value = entry.key || "";
keyInput.className = "api-key-value";
const showBtn = document.createElement("button");
showBtn.type = "button";
showBtn.className = "ghost";
showBtn.textContent = "Show";
showBtn.addEventListener("click", () => {
const isPassword = keyInput.type === "password";
keyInput.type = isPassword ? "text" : "password";
showBtn.textContent = isPassword ? "Hide" : "Show";
});
keyInline.appendChild(keyInput);
keyInline.appendChild(showBtn);
keyField.appendChild(keyLabel);
keyField.appendChild(keyInline);
const actions = document.createElement("div");
actions.className = "api-key-actions";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "accent add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => {
const first = apiKeysContainer.firstElementChild;
if (!first || first === card) return;
animateCardMove(card, () => {
apiKeysContainer.insertBefore(card, first);
}, { scrollToCenter: true });
updateApiKeyControls();
updateApiConfigKeyOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
animateCardMove(card, () => {
apiKeysContainer.insertBefore(card, previous);
});
updateApiKeyControls();
updateApiConfigKeyOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
animateCardMove(card, () => {
apiKeysContainer.insertBefore(card, next.nextElementSibling);
});
updateApiKeyControls();
updateApiConfigKeyOptions();
});
addBelowBtn.addEventListener("click", () => {
const name = buildUniqueDefaultName(
collectNames(apiKeysContainer, ".api-key-name")
);
const newCard = buildApiKeyCard({ id: newApiKeyId(), name, key: "" });
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
centerCardInView(newCard);
updateApiConfigKeyOptions();
updateApiKeyControls();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateApiConfigKeyOptions();
updateApiKeyControls();
});
actions.appendChild(deleteBtn);
const updateSelect = () => updateApiConfigKeyOptions();
nameInput.addEventListener("input", updateSelect);
keyInput.addEventListener("input", updateSelect);
body.appendChild(nameField);
body.appendChild(keyField);
summaryRight.appendChild(actions);
return card;
}
function collectApiKeys() {
const cards = [...apiKeysContainer.querySelectorAll(".api-key-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".api-key-name");
const keyInput = card.querySelector(".api-key-value");
const enabledInput = card.querySelector(".config-enabled");
return {
id: card.dataset.id || newApiKeyId(),
name: (nameInput?.value || "Default").trim(),
key: (keyInput?.value || "").trim(),
enabled: enabledInput ? enabledInput.checked : true
};
});
}
function updateApiKeyControls() {
const cards = [...apiKeysContainer.querySelectorAll(".api-key-card")];
cards.forEach((card, index) => {
const moveTopBtn = card.querySelector(".move-top");
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveTopBtn) moveTopBtn.disabled = index === 0;
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
scheduleSidebarErrors();
scheduleTocUpdate();
}
function updateApiConfigKeyOptions() {
const keys = collectApiKeys().filter((key) => isEnabled(key.enabled));
const selects = apiConfigsContainer.querySelectorAll(".api-config-key-select");
selects.forEach((select) => {
const preferred = select.dataset.preferred || select.value;
select.innerHTML = "";
if (!keys.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = "No keys configured";
select.appendChild(option);
select.disabled = true;
return;
}
select.disabled = false;
for (const key of keys) {
const option = document.createElement("option");
option.value = key.id;
option.textContent = key.name || "Default";
select.appendChild(option);
}
if (preferred && keys.some((key) => key.id === preferred)) {
select.value = preferred;
} else {
select.value = keys[0].id;
}
select.dataset.preferred = select.value;
});
}
function buildEnvConfigCard(config, container = envConfigsContainer) {
const card = document.createElement("details");
card.className = "env-config-card";
card.dataset.id = config.id || newEnvConfigId();
card.dataset.stateKey = `env:${card.dataset.id}`;
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.className = "config-enabled";
enabledInput.checked = config.enabled !== false;
enabledInput.addEventListener("change", () => {
updateEnvApiOptions();
});
enabledInput.classList.add("hidden");
const nameField = document.createElement("div");
nameField.className = "field";
const nameLabel = document.createElement("label");
nameLabel.textContent = "Name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = config.name || "";
nameInput.className = "env-config-name";
const { body, summaryLeft, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled"
);
const enabledToggle = buildEnabledToggleButton(enabledInput);
summaryLeft.prepend(enabledToggle);
summaryLeft.appendChild(enabledInput);
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const apiField = document.createElement("div");
apiField.className = "field";
const apiLabel = document.createElement("label");
apiLabel.textContent = "API config";
const apiSelect = document.createElement("select");
apiSelect.className = "env-config-api-select";
apiSelect.dataset.preferred = config.apiConfigId || "";
apiField.appendChild(apiLabel);
apiField.appendChild(apiSelect);
const promptField = document.createElement("div");
promptField.className = "field";
const promptLabel = document.createElement("label");
promptLabel.textContent = "System prompt";
const promptInput = document.createElement("textarea");
promptInput.rows = 8;
promptInput.value = config.systemPrompt || "";
promptInput.className = "env-config-prompt";
promptField.appendChild(promptLabel);
promptField.appendChild(promptInput);
const primaryRow = document.createElement("div");
primaryRow.className = "inline-fields two env-config-primary";
primaryRow.appendChild(nameField);
primaryRow.appendChild(apiField);
const actions = document.createElement("div");
actions.className = "env-config-actions";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "accent add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => {
const first = container.firstElementChild;
if (!first || first === card) return;
animateCardMove(card, () => {
container.insertBefore(card, first);
}, { scrollToCenter: true });
updateEnvControls(container);
updateTaskEnvOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
animateCardMove(card, () => {
container.insertBefore(card, previous);
});
updateEnvControls(container);
updateTaskEnvOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
animateCardMove(card, () => {
container.insertBefore(card, next.nextElementSibling);
});
updateEnvControls(container);
updateTaskEnvOptions();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
const moveControls = buildMoveControls("envs", card, container);
addBelowBtn.addEventListener("click", () => {
const name = buildUniqueNumberedName(
"New Environment",
collectNames(container, ".env-config-name")
);
const fallbackApiConfigId = getApiConfigsForEnvContainer(container)[0]?.id || "";
const newCard = buildEnvConfigCard({
id: newEnvConfigId(),
name,
apiConfigId: fallbackApiConfigId,
systemPrompt: DEFAULT_SYSTEM_PROMPT
}, container);
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
centerCardInView(newCard);
updateEnvApiOptions();
updateEnvControls(container);
updateTaskEnvOptions();
});
const getSourceData = () => ({
id: card.dataset.id,
name: nameInput.value || "Default",
apiConfigId: apiSelect.value || "",
systemPrompt: promptInput.value || "",
enabled: enabledInput.checked
});
const duplicateControls = buildDuplicateControls("envs", getSourceData, {
onHere: () => {
const sourceRect = card.getBoundingClientRect();
const newCard = buildDuplicateCard("envs", getSourceData(), container);
if (!newCard) return;
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
animateDuplicateFromRect(newCard, sourceRect);
updateEnvApiOptions();
updateEnvControls(container);
updateTaskEnvOptions();
scheduleSidebarErrors();
centerCardInViewAfterLayout(newCard);
},
sourceCard: card
});
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateEnvControls(container);
updateTaskEnvOptions();
});
actions.appendChild(moveControls);
actions.appendChild(duplicateControls);
actions.appendChild(addBelowBtn);
actions.appendChild(deleteBtn);
nameInput.addEventListener("input", () => updateEnvApiOptions());
body.appendChild(primaryRow);
body.appendChild(promptField);
summaryRight.appendChild(actions);
return card;
}
function updateEnvControls(container = envConfigsContainer) {
const cards = [...container.querySelectorAll(".env-config-card")];
cards.forEach((card, index) => {
const moveTopBtn = card.querySelector(".move-top");
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveTopBtn) moveTopBtn.disabled = index === 0;
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
scheduleSidebarErrors();
scheduleTocUpdate();
}
function updateTaskEnvOptionsForContainer(container, envs, allEnvsById) {
if (!container) return;
const selects = container.querySelectorAll(".task-env-select");
selects.forEach((select) => {
populateSelectPreserving(
select,
envs,
"No environments configured",
allEnvsById
);
});
}
function updateTaskEnvOptions() {
const allGlobalEnvs = collectEnvConfigs();
const enabledGlobalEnvs = allGlobalEnvs.filter((env) => isEnabled(env.enabled));
updateTaskEnvOptionsForContainer(
tasksContainer,
enabledGlobalEnvs,
buildItemMap(allGlobalEnvs)
);
const workspaceCards = document.querySelectorAll(".workspace-card");
workspaceCards.forEach((card) => {
const scope = getWorkspaceScopeData(card);
const workspaceEnvs = collectEnvConfigs(card.querySelector(".workspace-envs"));
const allEnvs = mergeById(allGlobalEnvs, workspaceEnvs);
updateTaskEnvOptionsForContainer(
card.querySelector(".workspace-tasks"),
scope.envs,
buildItemMap(allEnvs)
);
});
const siteCards = document.querySelectorAll(".site-card");
siteCards.forEach((card) => {
const scope = getSiteScopeData(card);
const workspaceId = card.querySelector(".site-workspace")?.value || "global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
const workspaceEnvs = workspaceCard
? collectEnvConfigs(workspaceCard.querySelector(".workspace-envs"))
: [];
const siteEnvs = collectEnvConfigs(card.querySelector(".site-envs"));
const allEnvs = mergeById(allGlobalEnvs, workspaceEnvs, siteEnvs);
updateTaskEnvOptionsForContainer(
card.querySelector(".site-tasks"),
scope.envs,
buildItemMap(allEnvs)
);
});
updateShortcutOptions();
refreshWorkspaceInheritedLists();
refreshSiteInheritedLists();
scheduleSidebarErrors();
}
function buildProfileCard(profile, container = profilesContainer) {
const card = document.createElement("details");
card.className = "profile-card";
card.dataset.id = profile.id || newProfileId();
card.dataset.stateKey = `profile:${card.dataset.id}`;
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.className = "config-enabled";
enabledInput.checked = profile.enabled !== false;
enabledInput.addEventListener("change", () => {
updateTaskProfileOptions();
});
enabledInput.classList.add("hidden");
const nameField = document.createElement("div");
nameField.className = "field";
const nameLabel = document.createElement("label");
nameLabel.textContent = "Name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = profile.name || "";
nameInput.className = "profile-name";
const { body, summaryLeft, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled"
);
const enabledToggle = buildEnabledToggleButton(enabledInput);
summaryLeft.prepend(enabledToggle);
summaryLeft.appendChild(enabledInput);
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const textField = document.createElement("div");
textField.className = "field";
const textLabel = document.createElement("label");
textLabel.textContent = "Profile text";
const textArea = document.createElement("textarea");
textArea.rows = 8;
textArea.value = profile.text || "";
textArea.className = "profile-text";
textField.appendChild(textLabel);
textField.appendChild(textArea);
const actions = document.createElement("div");
actions.className = "profile-actions";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "accent add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => {
const first = container.firstElementChild;
if (!first || first === card) return;
animateCardMove(card, () => {
container.insertBefore(card, first);
}, { scrollToCenter: true });
updateProfileControls(container);
updateTaskProfileOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
animateCardMove(card, () => {
container.insertBefore(card, previous);
});
updateProfileControls(container);
updateTaskProfileOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
animateCardMove(card, () => {
container.insertBefore(card, next.nextElementSibling);
});
updateProfileControls(container);
updateTaskProfileOptions();
});
addBelowBtn.addEventListener("click", () => {
const name = buildUniqueNumberedName(
"New Profile",
collectNames(container, ".profile-name")
);
const newCard = buildProfileCard({
id: newProfileId(),
name,
text: ""
}, container);
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
centerCardInView(newCard);
updateProfileControls(container);
updateTaskProfileOptions();
});
const getSourceData = () => ({
id: card.dataset.id,
name: nameInput.value || "Default",
text: textArea.value || "",
enabled: enabledInput.checked
});
const duplicateControls = buildDuplicateControls("profiles", getSourceData, {
onHere: () => {
const sourceRect = card.getBoundingClientRect();
const newCard = buildDuplicateCard("profiles", getSourceData(), container);
if (!newCard) return;
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
animateDuplicateFromRect(newCard, sourceRect);
updateProfileControls(container);
updateTaskProfileOptions();
scheduleSidebarErrors();
centerCardInViewAfterLayout(newCard);
},
sourceCard: card
});
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateProfileControls(container);
updateTaskProfileOptions();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
const moveControls = buildMoveControls("profiles", card, container);
actions.appendChild(moveControls);
actions.appendChild(duplicateControls);
actions.appendChild(addBelowBtn);
actions.appendChild(deleteBtn);
nameInput.addEventListener("input", () => updateTaskProfileOptions());
body.appendChild(nameField);
body.appendChild(textField);
summaryRight.appendChild(actions);
return card;
}
function collectProfiles(container = profilesContainer) {
if (!container) return [];
const cards = [...container.querySelectorAll(".profile-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".profile-name");
const textArea = card.querySelector(".profile-text");
const enabledInput = card.querySelector(".config-enabled");
return {
id: card.dataset.id || newProfileId(),
name: (nameInput?.value || "Default").trim(),
text: (textArea?.value || "").trim(),
enabled: enabledInput ? enabledInput.checked : true
};
});
}
function updateProfileControls(container = profilesContainer) {
const cards = [...container.querySelectorAll(".profile-card")];
cards.forEach((card, index) => {
const moveTopBtn = card.querySelector(".move-top");
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveTopBtn) moveTopBtn.disabled = index === 0;
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
scheduleSidebarErrors();
scheduleTocUpdate();
}
function updateTaskProfileOptionsForContainer(container, profiles, allProfilesById) {
if (!container) return;
const selects = container.querySelectorAll(".task-profile-select");
selects.forEach((select) => {
populateSelectPreserving(
select,
profiles,
"No profiles configured",
allProfilesById
);
});
}
function updateTaskProfileOptions() {
const allGlobalProfiles = collectProfiles();
const enabledProfiles = allGlobalProfiles.filter((profile) =>
isEnabled(profile.enabled)
);
updateTaskProfileOptionsForContainer(
tasksContainer,
enabledProfiles,
buildItemMap(allGlobalProfiles)
);
const workspaceCards = document.querySelectorAll(".workspace-card");
workspaceCards.forEach((card) => {
const scope = getWorkspaceScopeData(card);
const workspaceProfiles = collectProfiles(
card.querySelector(".workspace-profiles")
);
const allProfiles = mergeById(allGlobalProfiles, workspaceProfiles);
updateTaskProfileOptionsForContainer(
card.querySelector(".workspace-tasks"),
scope.profiles,
buildItemMap(allProfiles)
);
});
const siteCards = document.querySelectorAll(".site-card");
siteCards.forEach((card) => {
const scope = getSiteScopeData(card);
const workspaceId = card.querySelector(".site-workspace")?.value || "global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
const workspaceProfiles = workspaceCard
? collectProfiles(workspaceCard.querySelector(".workspace-profiles"))
: [];
const siteProfiles = collectProfiles(card.querySelector(".site-profiles"));
const allProfiles = mergeById(
allGlobalProfiles,
workspaceProfiles,
siteProfiles
);
updateTaskProfileOptionsForContainer(
card.querySelector(".site-tasks"),
scope.profiles,
buildItemMap(allProfiles)
);
});
updateShortcutOptions();
refreshWorkspaceInheritedLists();
refreshSiteInheritedLists();
scheduleSidebarErrors();
}
function updateEnvApiOptionsForContainer(container, apiConfigs) {
if (!container) return;
const selects = container.querySelectorAll(".env-config-api-select");
selects.forEach((select) => {
if (select.value) {
select.dataset.preferred = select.value;
}
populateSelect(select, apiConfigs, "No API configs configured");
});
}
function refreshWorkspaceApiConfigLists() {
const apiConfigs = collectApiConfigs().filter((config) => isEnabled(config.enabled));
const workspaceCards = document.querySelectorAll(".workspace-card");
workspaceCards.forEach((card) => {
const list = card.querySelector('.inherited-list[data-module="apiConfigs"]');
if (!list) return;
const disabled = collectDisabledInherited(list);
const nextList = buildApiConfigToggleList(apiConfigs, disabled);
nextList.dataset.module = "apiConfigs";
list.replaceWith(nextList);
});
}
function refreshSiteApiConfigLists() {
const siteCards = document.querySelectorAll(".site-card");
siteCards.forEach((card) => {
const list = card.querySelector('.inherited-list[data-module="apiConfigs"]');
if (!list) return;
const disabled = collectDisabledInherited(list);
const scopedConfigs = getWorkspaceAvailableApiConfigsForSite(card);
const nextList = buildApiConfigToggleList(scopedConfigs, disabled);
nextList.dataset.module = "apiConfigs";
list.replaceWith(nextList);
});
}
function refreshWorkspaceInheritedLists() {
const workspaceCards = document.querySelectorAll(".workspace-card");
workspaceCards.forEach((card) => {
const sections = [
{
key: "envs",
parent: () => collectEnvConfigs(),
container: card.querySelector(".workspace-envs")
},
{
key: "profiles",
parent: () => collectProfiles(),
container: card.querySelector(".workspace-profiles")
},
{
key: "tasks",
parent: () => collectTasks(),
container: card.querySelector(".workspace-tasks")
},
{
key: "shortcuts",
parent: () => collectShortcuts(),
container: card.querySelector(".workspace-shortcuts")
}
];
sections.forEach((section) => {
const list = card.querySelector(
`.inherited-list[data-module="${section.key}"]`
);
if (!list) return;
replaceInheritedList(
list,
section.key,
section.parent,
section.container
);
});
});
refreshInheritedSourceLabels();
}
function refreshSiteInheritedLists() {
const siteCards = document.querySelectorAll(".site-card");
siteCards.forEach((card) => {
const workspaceId = card.querySelector(".site-workspace")?.value || "global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
const workspaceScope = workspaceCard
? getWorkspaceScopeData(workspaceCard)
: {
envs: collectEnvConfigs(),
profiles: collectProfiles(),
tasks: collectTasks(),
shortcuts: collectShortcuts()
};
const sections = [
{
key: "envs",
parent: workspaceScope.envs,
container: card.querySelector(".site-envs")
},
{
key: "profiles",
parent: workspaceScope.profiles,
container: card.querySelector(".site-profiles")
},
{
key: "tasks",
parent: workspaceScope.tasks,
container: card.querySelector(".site-tasks")
},
{
key: "shortcuts",
parent: workspaceScope.shortcuts,
container: card.querySelector(".site-shortcuts")
}
];
sections.forEach((section) => {
const list = card.querySelector(
`.inherited-list[data-module="${section.key}"]`
);
if (!list) return;
replaceInheritedList(list, section.key, section.parent, section.container);
});
});
refreshInheritedSourceLabels();
}
function getWorkspaceApiConfigs(workspaceCard) {
const apiConfigs = collectApiConfigs().filter((config) => isEnabled(config.enabled));
if (!workspaceCard) return apiConfigs;
const disabled = collectDisabledInherited(
workspaceCard.querySelector('.inherited-list[data-module="apiConfigs"]')
);
return apiConfigs.filter((config) => !disabled.includes(config.id));
}
function getSiteApiConfigs(siteCard) {
const apiConfigs = collectApiConfigs().filter((config) => isEnabled(config.enabled));
if (!siteCard) return apiConfigs;
const workspaceId =
siteCard.querySelector(".site-workspace")?.value || "global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
const workspaceDisabled = collectDisabledInherited(
workspaceCard?.querySelector('.inherited-list[data-module="apiConfigs"]')
);
const siteDisabled = collectDisabledInherited(
siteCard.querySelector('.inherited-list[data-module="apiConfigs"]')
);
return apiConfigs.filter(
(config) =>
!workspaceDisabled.includes(config.id) &&
!siteDisabled.includes(config.id)
);
}
function getWorkspaceAvailableApiConfigsForSite(siteCard) {
const apiConfigs = collectApiConfigs().filter((config) => isEnabled(config.enabled));
if (!siteCard) return apiConfigs;
const workspaceId =
siteCard.querySelector(".site-workspace")?.value || "global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
const workspaceDisabled = collectDisabledInherited(
workspaceCard?.querySelector('.inherited-list[data-module="apiConfigs"]')
);
return apiConfigs.filter((config) => !workspaceDisabled.includes(config.id));
}
function getApiConfigsForEnvContainer(container) {
if (!container) {
return collectApiConfigs().filter((config) => isEnabled(config.enabled));
}
const workspaceCard = container.closest(".workspace-card");
if (workspaceCard) {
return getWorkspaceApiConfigs(workspaceCard);
}
const siteCard = container.closest(".site-card");
if (siteCard) {
return getSiteApiConfigs(siteCard);
}
return collectApiConfigs().filter((config) => isEnabled(config.enabled));
}
function getTaskScopeForContainer(container) {
if (!container) {
return {
envs: collectEnvConfigs().filter((env) => isEnabled(env.enabled)),
profiles: collectProfiles().filter((profile) => isEnabled(profile.enabled))
};
}
const siteCard = container.closest(".site-card");
if (siteCard) {
const scope = getSiteScopeData(siteCard);
return { envs: scope.envs, profiles: scope.profiles };
}
const workspaceCard = container.closest(".workspace-card");
if (workspaceCard) {
const scope = getWorkspaceScopeData(workspaceCard);
return { envs: scope.envs, profiles: scope.profiles };
}
return {
envs: collectEnvConfigs().filter((env) => isEnabled(env.enabled)),
profiles: collectProfiles().filter((profile) => isEnabled(profile.enabled))
};
}
function updateEnvApiOptions() {
refreshWorkspaceApiConfigLists();
refreshSiteApiConfigLists();
const apiConfigs = collectApiConfigs().filter((config) => isEnabled(config.enabled));
updateEnvApiOptionsForContainer(envConfigsContainer, apiConfigs);
const workspaceCards = document.querySelectorAll(".workspace-card");
workspaceCards.forEach((card) => {
const scopedConfigs = getWorkspaceApiConfigs(card);
updateEnvApiOptionsForContainer(
card.querySelector(".workspace-envs"),
scopedConfigs
);
});
const siteCards = document.querySelectorAll(".site-card");
siteCards.forEach((card) => {
const scopedConfigs = getSiteApiConfigs(card);
updateEnvApiOptionsForContainer(
card.querySelector(".site-envs"),
scopedConfigs
);
});
updateTaskEnvOptions();
refreshWorkspaceInheritedLists();
refreshSiteInheritedLists();
}
function updateShortcutOptionsForContainer(container, options = {}) {
if (!container) return;
const envs = options.envs || [];
const profiles = options.profiles || [];
const tasks = options.tasks || [];
const cards = container.querySelectorAll(".shortcut-card");
cards.forEach((card) => {
const envSelect = card.querySelector(".shortcut-env");
const profileSelect = card.querySelector(".shortcut-profile");
const taskSelect = card.querySelector(".shortcut-task");
if (envSelect) {
if (envSelect.value) {
envSelect.dataset.preferred = envSelect.value;
}
populateSelect(envSelect, envs, "No environments configured");
}
if (profileSelect) {
if (profileSelect.value) {
profileSelect.dataset.preferred = profileSelect.value;
}
populateSelect(profileSelect, profiles, "No profiles configured");
}
if (taskSelect) {
if (taskSelect.value) {
taskSelect.dataset.preferred = taskSelect.value;
}
populateSelect(taskSelect, tasks, "No tasks configured");
}
});
updateShortcutControls(container);
}
function updateShortcutOptions() {
const envs = collectEnvConfigs().filter((env) => isEnabled(env.enabled));
const profiles = collectProfiles().filter((profile) => isEnabled(profile.enabled));
const tasks = collectTasks().filter((task) => isEnabled(task.enabled));
updateShortcutOptionsForContainer(shortcutsContainer, { envs, profiles, tasks });
const workspaceCards = document.querySelectorAll(".workspace-card");
workspaceCards.forEach((card) => {
const scope = getWorkspaceScopeData(card);
updateShortcutOptionsForContainer(card.querySelector(".workspace-shortcuts"), {
envs: scope.envs,
profiles: scope.profiles,
tasks: scope.tasks
});
});
const siteCards = document.querySelectorAll(".site-card");
siteCards.forEach((card) => {
const scope = getSiteScopeData(card);
updateShortcutOptionsForContainer(card.querySelector(".site-shortcuts"), {
envs: scope.envs,
profiles: scope.profiles,
tasks: scope.tasks
});
});
refreshWorkspaceInheritedLists();
refreshSiteInheritedLists();
scheduleSidebarErrors();
}
function collectWorkspaces() {
const cards = [...workspacesContainer.querySelectorAll(".workspace-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".workspace-name");
const themeSelect = card.querySelector(".appearance-theme");
const toolbarSelect = card.querySelector(".appearance-toolbar-position");
const defaultEnvProfileSelect = card.querySelector(
".appearance-default-env-profile"
);
const emptyToolbarSelect = card.querySelector(".appearance-empty-toolbar");
// Collect nested resources
const envsContainer = card.querySelector(".workspace-envs");
const profilesContainer = card.querySelector(".workspace-profiles");
const tasksContainer = card.querySelector(".workspace-tasks");
const shortcutsContainer = card.querySelector(".workspace-shortcuts");
const envsInherited = card.querySelector('.inherited-list[data-module="envs"]');
const profilesInherited = card.querySelector('.inherited-list[data-module="profiles"]');
const tasksInherited = card.querySelector('.inherited-list[data-module="tasks"]');
const shortcutsInherited = card.querySelector('.inherited-list[data-module="shortcuts"]');
const apiConfigsInherited = card.querySelector('.inherited-list[data-module="apiConfigs"]');
// We can reuse collect functions if they accept a container!
// But collectEnvConfigs currently returns objects with flat IDs.
// We'll need to ensure we don't lose the nested nature or we handle it during save.
// Actually, saveSettings stores workspaces array. If we put the resources inside, it works.
return {
id: card.dataset.id || newWorkspaceId(),
name: (nameInput?.value || "Untitled Workspace").trim(),
theme: themeSelect?.value || "inherit",
toolbarPosition: toolbarSelect?.value || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
defaultEnvProfileSelect?.value
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
emptyToolbarSelect?.value
),
envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [],
profiles: profilesContainer ? collectProfiles(profilesContainer) : [],
tasks: tasksContainer ? collectTasks(tasksContainer) : [],
shortcuts: shortcutsContainer ? collectShortcuts(shortcutsContainer) : [],
disabledInherited: {
envs: collectDisabledInherited(envsInherited),
profiles: collectDisabledInherited(profilesInherited),
tasks: collectDisabledInherited(tasksInherited),
shortcuts: collectDisabledInherited(shortcutsInherited),
apiConfigs: collectDisabledInherited(apiConfigsInherited)
}
};
});
}
function collectShortcuts(container = shortcutsContainer) {
if (!container) return [];
const cards = [...container.querySelectorAll(".shortcut-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".shortcut-name");
const envSelect = card.querySelector(".shortcut-env");
const profileSelect = card.querySelector(".shortcut-profile");
const taskSelect = card.querySelector(".shortcut-task");
const enabledInput = card.querySelector(".config-enabled");
return {
id: card.dataset.id || newShortcutId(),
name: (nameInput?.value || "Untitled Shortcut").trim(),
envId: envSelect?.value || "",
profileId: profileSelect?.value || "",
taskId: taskSelect?.value || "",
enabled: enabledInput ? enabledInput.checked : true
};
});
}
function collectEnvConfigs(container = envConfigsContainer) {
if (!container) return [];
const cards = [...container.querySelectorAll(".env-config-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".env-config-name");
const apiSelect = card.querySelector(".env-config-api-select");
const promptInput = card.querySelector(".env-config-prompt");
const enabledInput = card.querySelector(".config-enabled");
return {
id: card.dataset.id || newEnvConfigId(),
name: (nameInput?.value || "Default").trim(),
apiConfigId: apiSelect?.value || "",
systemPrompt: (promptInput?.value || "").trim(),
enabled: enabledInput ? enabledInput.checked : true
};
});
}
function renderWorkspaceSection(title, containerClass, items, builder, newItemFactory) {
const details = document.createElement("details");
details.className = "panel sub-panel";
details.style.marginTop = "10px";
details.style.border = "1px solid var(--border)";
details.style.borderRadius = "8px";
details.style.padding = "8px";
const summary = document.createElement("summary");
summary.className = "panel-summary";
summary.style.cursor = "pointer";
const summaryRow = document.createElement("div");
summaryRow.className = "panel-summary-row";
const summaryLeft = document.createElement("div");
summaryLeft.className = "panel-summary-left";
const summaryRight = document.createElement("div");
summaryRight.className = "panel-summary-right";
const summaryTitle = document.createElement("h3");
summaryTitle.textContent = title;
summaryTitle.style.display = "inline";
summaryTitle.style.fontSize = "13px";
summaryTitle.style.fontWeight = "600";
summaryTitle.style.margin = "0";
summaryLeft.appendChild(summaryTitle);
summaryRow.appendChild(summaryLeft);
summaryRow.appendChild(summaryRight);
summary.appendChild(summaryRow);
details.appendChild(summary);
const body = document.createElement("div");
body.className = "panel-body panel-body-inherited";
body.style.paddingTop = "10px";
const listContainer = document.createElement("div");
listContainer.className = containerClass;
if (items && Array.isArray(items)) {
for (const item of items) {
listContainer.appendChild(builder(item, listContainer));
}
}
const addBtn = document.createElement("button");
addBtn.className = "accent";
addBtn.type = "button";
addBtn.textContent = "Add";
addBtn.addEventListener("click", () => {
openDetails(details);
const newItem = newItemFactory(listContainer);
const newCard = builder(newItem, listContainer);
listContainer.appendChild(newCard);
openDetails(newCard);
centerCardInView(newCard);
scheduleSidebarErrors();
});
summaryRight.appendChild(addBtn);
body.appendChild(listContainer);
details.appendChild(body);
return details;
}
const THEME_LABELS = {
system: "System",
light: "Light",
dark: "Dark"
};
const TOOLBAR_POSITION_LABELS = {
"bottom-right": "Bottom Right",
"bottom-left": "Bottom Left",
"top-right": "Top Right",
"top-left": "Top Left",
"bottom-center": "Bottom Center"
};
const EMPTY_TOOLBAR_BEHAVIOR_LABELS = {
hide: "Hide Toolbar",
open: 'Show "Open SiteCompanion"'
};
function normalizeAppearanceToggle(value) {
if (value === "inherit" || value === "enabled" || value === "disabled") {
return value;
}
if (value === true) return "enabled";
if (value === false) return "disabled";
return "inherit";
}
function normalizeEmptyToolbarBehavior(value, allowInherit = true) {
if (value === "hide" || value === "open") return value;
if (allowInherit && value === "inherit") return "inherit";
return allowInherit ? "inherit" : "open";
}
function resolveAppearanceToggleValue(value, fallback) {
const normalized = normalizeAppearanceToggle(value);
if (normalized === "inherit") return Boolean(fallback);
return normalized === "enabled";
}
function getThemeLabel(value) {
return THEME_LABELS[value] || String(value || "System");
}
function getToolbarPositionLabel(value) {
return TOOLBAR_POSITION_LABELS[value] || String(value || "Bottom Right");
}
function getDefaultEnvProfileLabel(value) {
return value ? "Enabled" : "Disabled";
}
function getEmptyToolbarBehaviorLabel(value) {
return EMPTY_TOOLBAR_BEHAVIOR_LABELS[value] || "Hide Toolbar";
}
function getGlobalAppearanceConfig() {
return {
theme: themeSelect?.value || "system",
toolbarPosition: toolbarPositionSelect?.value || "bottom-right",
alwaysUseDefaultEnvProfile: resolveAppearanceToggleValue(
alwaysUseDefaultEnvProfileSelect?.value,
false
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
emptyToolbarBehaviorSelect?.value,
false
)
};
}
function updateAppearanceInheritedHint(selectEl, hintEl, label) {
if (!selectEl || !hintEl) return;
if (selectEl.value !== "inherit") {
hintEl.textContent = "Not inheriting";
hintEl.classList.remove("hidden");
return;
}
hintEl.textContent = `Inherited: ${label}`;
hintEl.classList.remove("hidden");
}
function updateAppearanceInheritanceIndicators() {
const global = getGlobalAppearanceConfig();
const workspaceCards = document.querySelectorAll(".workspace-card");
const workspaceAppearance = new Map();
workspaceCards.forEach((card) => {
const themeSelect = card.querySelector(".appearance-theme");
const toolbarSelect = card.querySelector(".appearance-toolbar-position");
const defaultSelect = card.querySelector(".appearance-default-env-profile");
const emptyToolbarSelect = card.querySelector(".appearance-empty-toolbar");
const themeValue = themeSelect?.value || "inherit";
const toolbarValue = toolbarSelect?.value || "inherit";
const defaultValue = defaultSelect?.value || "inherit";
const emptyToolbarValue = normalizeEmptyToolbarBehavior(
emptyToolbarSelect?.value || "inherit"
);
const resolvedTheme =
themeValue === "inherit" ? global.theme : themeValue;
const resolvedToolbar =
toolbarValue === "inherit" ? global.toolbarPosition : toolbarValue;
const resolvedDefault = resolveAppearanceToggleValue(
defaultValue,
global.alwaysUseDefaultEnvProfile
);
const resolvedEmptyToolbar =
emptyToolbarValue === "inherit"
? global.emptyToolbarBehavior
: emptyToolbarValue;
workspaceAppearance.set(card.dataset.id, {
theme: resolvedTheme,
toolbarPosition: resolvedToolbar,
alwaysUseDefaultEnvProfile: resolvedDefault,
emptyToolbarBehavior: resolvedEmptyToolbar
});
updateAppearanceInheritedHint(
themeSelect,
card.querySelector('.appearance-inherited[data-appearance-key="theme"]'),
getThemeLabel(global.theme)
);
updateAppearanceInheritedHint(
toolbarSelect,
card.querySelector(
'.appearance-inherited[data-appearance-key="toolbarPosition"]'
),
getToolbarPositionLabel(global.toolbarPosition)
);
updateAppearanceInheritedHint(
defaultSelect,
card.querySelector(
'.appearance-inherited[data-appearance-key="alwaysUseDefaultEnvProfile"]'
),
getDefaultEnvProfileLabel(global.alwaysUseDefaultEnvProfile)
);
updateAppearanceInheritedHint(
emptyToolbarSelect,
card.querySelector(
'.appearance-inherited[data-appearance-key="emptyToolbarBehavior"]'
),
getEmptyToolbarBehaviorLabel(global.emptyToolbarBehavior)
);
});
const siteCards = document.querySelectorAll(".site-card");
siteCards.forEach((card) => {
const workspaceId = card.querySelector(".site-workspace")?.value || "global";
const resolved =
workspaceAppearance.get(workspaceId) || global;
updateAppearanceInheritedHint(
card.querySelector(".appearance-theme"),
card.querySelector('.appearance-inherited[data-appearance-key="theme"]'),
getThemeLabel(resolved.theme)
);
updateAppearanceInheritedHint(
card.querySelector(".appearance-toolbar-position"),
card.querySelector(
'.appearance-inherited[data-appearance-key="toolbarPosition"]'
),
getToolbarPositionLabel(resolved.toolbarPosition)
);
updateAppearanceInheritedHint(
card.querySelector(".appearance-default-env-profile"),
card.querySelector(
'.appearance-inherited[data-appearance-key="alwaysUseDefaultEnvProfile"]'
),
getDefaultEnvProfileLabel(resolved.alwaysUseDefaultEnvProfile)
);
updateAppearanceInheritedHint(
card.querySelector(".appearance-empty-toolbar"),
card.querySelector(
'.appearance-inherited[data-appearance-key="emptyToolbarBehavior"]'
),
getEmptyToolbarBehaviorLabel(resolved.emptyToolbarBehavior)
);
});
}
function buildAppearanceSection(
{
theme = "inherit",
toolbarPosition = "inherit",
alwaysUseDefaultEnvProfile = "inherit",
emptyToolbarBehavior = "inherit"
} = {},
{ stateKey } = {}
) {
const details = document.createElement("details");
details.className = "panel sub-panel";
if (stateKey) {
details.dataset.stateKey = stateKey;
}
const summary = document.createElement("summary");
summary.className = "panel-summary";
const summaryRow = document.createElement("div");
summaryRow.className = "panel-summary-row";
const summaryLeft = document.createElement("div");
summaryLeft.className = "panel-summary-left";
const summaryRight = document.createElement("div");
summaryRight.className = "panel-summary-right";
const summaryTitle = document.createElement("h3");
summaryTitle.textContent = "Appearance";
summaryTitle.style.display = "inline";
summaryTitle.style.fontSize = "13px";
summaryTitle.style.fontWeight = "600";
summaryTitle.style.margin = "0";
summaryLeft.appendChild(summaryTitle);
summaryRow.appendChild(summaryLeft);
summaryRow.appendChild(summaryRight);
summary.appendChild(summaryRow);
details.appendChild(summary);
const body = document.createElement("div");
body.className = "panel-body";
const themeField = document.createElement("div");
themeField.className = "field";
const themeLabel = document.createElement("label");
themeLabel.textContent = "Theme";
const themeSelect = document.createElement("select");
themeSelect.className = "appearance-theme";
const themes = ["inherit", "system", "light", "dark"];
for (const t of themes) {
const opt = document.createElement("option");
opt.value = t;
opt.textContent = t.charAt(0).toUpperCase() + t.slice(1);
themeSelect.appendChild(opt);
}
themeSelect.value = theme || "inherit";
themeField.appendChild(themeLabel);
themeField.appendChild(themeSelect);
const themeHint = document.createElement("div");
themeHint.className = "hint appearance-inherited hidden";
themeHint.dataset.appearanceKey = "theme";
themeField.appendChild(themeHint);
const toolbarField = document.createElement("div");
toolbarField.className = "field";
const toolbarLabel = document.createElement("label");
toolbarLabel.textContent = "Toolbar position";
const toolbarSelect = document.createElement("select");
toolbarSelect.className = "appearance-toolbar-position";
const positions = [
"inherit",
"bottom-right",
"bottom-left",
"top-right",
"top-left",
"bottom-center"
];
const positionLabels = {
inherit: "Inherit",
"bottom-right": "Bottom Right",
"bottom-left": "Bottom Left",
"top-right": "Top Right",
"top-left": "Top Left",
"bottom-center": "Bottom Center"
};
for (const pos of positions) {
const opt = document.createElement("option");
opt.value = pos;
opt.textContent = positionLabels[pos] || pos;
toolbarSelect.appendChild(opt);
}
toolbarSelect.value = toolbarPosition || "inherit";
toolbarField.appendChild(toolbarLabel);
toolbarField.appendChild(toolbarSelect);
const toolbarHint = document.createElement("div");
toolbarHint.className = "hint appearance-inherited hidden";
toolbarHint.dataset.appearanceKey = "toolbarPosition";
toolbarField.appendChild(toolbarHint);
const defaultField = document.createElement("div");
defaultField.className = "field";
const defaultLabel = document.createElement("label");
defaultLabel.textContent = "Always use default ENV/PROFILE";
const defaultSelect = document.createElement("select");
defaultSelect.className = "appearance-default-env-profile";
const defaultOptions = [
{ value: "inherit", label: "Inherit" },
{ value: "enabled", label: "Enabled" },
{ value: "disabled", label: "Disabled" }
];
for (const optValue of defaultOptions) {
const opt = document.createElement("option");
opt.value = optValue.value;
opt.textContent = optValue.label;
defaultSelect.appendChild(opt);
}
defaultSelect.value = normalizeAppearanceToggle(alwaysUseDefaultEnvProfile);
defaultField.appendChild(defaultLabel);
defaultField.appendChild(defaultSelect);
const defaultHint = document.createElement("div");
defaultHint.className = "hint appearance-inherited hidden";
defaultHint.dataset.appearanceKey = "alwaysUseDefaultEnvProfile";
defaultField.appendChild(defaultHint);
const emptyToolbarField = document.createElement("div");
emptyToolbarField.className = "field";
const emptyToolbarLabel = document.createElement("label");
emptyToolbarLabel.textContent = "Empty toolbar";
const emptyToolbarSelect = document.createElement("select");
emptyToolbarSelect.className = "appearance-empty-toolbar";
const emptyToolbarOptions = [
{ value: "inherit", label: "Inherit" },
{ value: "hide", label: "Hide Toolbar" },
{ value: "open", label: 'Show "Open SiteCompanion"' }
];
for (const optionConfig of emptyToolbarOptions) {
const opt = document.createElement("option");
opt.value = optionConfig.value;
opt.textContent = optionConfig.label;
emptyToolbarSelect.appendChild(opt);
}
emptyToolbarSelect.value = normalizeEmptyToolbarBehavior(
emptyToolbarBehavior
);
emptyToolbarField.appendChild(emptyToolbarLabel);
emptyToolbarField.appendChild(emptyToolbarSelect);
const emptyToolbarHint = document.createElement("div");
emptyToolbarHint.className = "hint appearance-inherited hidden";
emptyToolbarHint.dataset.appearanceKey = "emptyToolbarBehavior";
emptyToolbarField.appendChild(emptyToolbarHint);
const appearanceRow = document.createElement("div");
appearanceRow.className = "inline-fields four appearance-fields";
appearanceRow.appendChild(themeField);
appearanceRow.appendChild(toolbarField);
appearanceRow.appendChild(defaultField);
appearanceRow.appendChild(emptyToolbarField);
body.appendChild(appearanceRow);
details.appendChild(body);
registerDetail(details, details.open);
return details;
}
function normalizeName(value) {
return (value || "").trim().toLowerCase();
}
function buildRenameMap(previousItems, nextItems) {
const previous = Array.isArray(previousItems) ? previousItems : [];
const next = Array.isArray(nextItems) ? nextItems : [];
const previousById = new Map();
for (const item of previous) {
const id = item?.id;
const name = normalizeName(item?.name);
if (id && name) previousById.set(id, name);
}
const map = new Map();
for (const item of next) {
const id = item?.id;
if (!id || !previousById.has(id)) continue;
const previousName = previousById.get(id);
const nextName = normalizeName(item?.name);
if (!previousName || !nextName || previousName === nextName) continue;
map.set(previousName, nextName);
}
return map;
}
function applyRenameMap(list, map) {
if (!Array.isArray(list) || !map || map.size === 0) {
return Array.isArray(list) ? [...list] : [];
}
const output = [];
const seen = new Set();
for (const raw of list) {
const normalized = normalizeName(raw);
if (!normalized) continue;
const next = map.get(normalized) || normalized;
if (seen.has(next)) continue;
seen.add(next);
output.push(next);
}
return output;
}
function applyRenameMaps(list, maps) {
let output = Array.isArray(list) ? [...list] : [];
for (const map of maps) {
if (map && map.size) {
output = applyRenameMap(output, map);
}
}
return output;
}
function filterDisabledIds(list, items) {
if (!Array.isArray(list)) return [];
const allowed = new Set(
(Array.isArray(items) ? items : [])
.map((item) => item?.id)
.filter(Boolean)
);
return list.filter((id) => allowed.has(id));
}
function resolveScopedItems(parentItems, localItems, disabledNames) {
const parent = Array.isArray(parentItems) ? parentItems : [];
const local = Array.isArray(localItems) ? localItems : [];
const disabledSet = new Set(
(disabledNames || []).map((name) => normalizeName(name)).filter(Boolean)
);
const localNameSet = new Set(
local.map((item) => normalizeName(item.name)).filter(Boolean)
);
const inherited = parent.filter((item) => {
if (!isEnabled(item.enabled)) return false;
const key = normalizeName(item.name);
if (!key) return false;
if (localNameSet.has(key)) return false;
if (disabledSet.has(key)) return false;
return true;
});
const effective = [
...inherited,
...local.filter((item) => isEnabled(item.enabled))
];
return { inherited, effective, localNameSet, disabledSet };
}
function buildInheritedList(parentItems, localItems, disabledNames) {
const container = document.createElement("div");
container.className = "inherited-list";
const parent = Array.isArray(parentItems) ? parentItems : [];
const local = Array.isArray(localItems) ? localItems : [];
const localNameSet = new Set(
local.map((item) => normalizeName(item.name)).filter(Boolean)
);
const disabledSet = new Set(
(disabledNames || []).map((name) => normalizeName(name)).filter(Boolean)
);
const enabledParents = parent.filter((item) => isEnabled(item.enabled));
if (!enabledParents.length) {
const empty = document.createElement("div");
empty.className = "hint";
empty.textContent = "No inherited items.";
container.appendChild(empty);
return container;
}
for (const item of enabledParents) {
const key = normalizeName(item.name);
if (!key) continue;
const overridden = localNameSet.has(key);
const disabled = overridden || disabledSet.has(key);
const row = document.createElement("div");
row.className = "inherited-item";
row.dataset.key = key;
row.dataset.overridden = overridden ? "true" : "false";
row.classList.toggle("is-enabled", !disabled);
row.classList.toggle("is-disabled", disabled);
const label = document.createElement("label");
label.className = "inherited-button";
const toggle = document.createElement("input");
toggle.type = "checkbox";
toggle.className = "inherited-toggle";
toggle.checked = !disabled;
toggle.disabled = overridden;
toggle.addEventListener("change", () => {
const enabled = toggle.checked;
row.classList.toggle("is-enabled", enabled);
row.classList.toggle("is-disabled", !enabled);
});
label.appendChild(toggle);
label.appendChild(document.createTextNode(item.name || "Untitled"));
row.appendChild(label);
if (overridden) {
const helper = document.createElement("div");
helper.className = "hint";
helper.textContent = "Overridden by a local config.";
row.appendChild(helper);
}
container.appendChild(row);
}
return container;
}
function buildApiConfigToggleList(apiConfigs, disabledIds) {
const container = document.createElement("div");
container.className = "inherited-list";
const configs = (apiConfigs || []).filter((config) => isEnabled(config.enabled));
const disabledSet = new Set(disabledIds || []);
if (!configs.length) {
const empty = document.createElement("div");
empty.className = "hint";
empty.textContent = "No API configs available.";
container.appendChild(empty);
return container;
}
for (const config of configs) {
const row = document.createElement("div");
row.className = "inherited-item";
row.dataset.key = config.id;
const enabled = !disabledSet.has(config.id);
row.classList.toggle("is-enabled", enabled);
row.classList.toggle("is-disabled", !enabled);
const label = document.createElement("label");
label.className = "inherited-button";
const toggle = document.createElement("input");
toggle.type = "checkbox";
toggle.className = "inherited-toggle";
toggle.checked = enabled;
toggle.addEventListener("change", () => {
row.classList.toggle("is-enabled", toggle.checked);
row.classList.toggle("is-disabled", !toggle.checked);
updateEnvApiOptions();
scheduleSidebarErrors();
});
label.appendChild(toggle);
label.appendChild(document.createTextNode(config.name || "Default"));
row.appendChild(label);
container.appendChild(row);
}
return container;
}
function setInheritedSource(group, source) {
if (!group) return;
const heading = group.querySelector(".scope-title");
if (!heading) return;
let meta = heading.querySelector(".scope-meta-link");
if (!source || !source.label) {
if (meta) meta.remove();
return;
}
if (!meta) {
meta = document.createElement("button");
meta.type = "button";
meta.className = "scope-meta-link hint-accent";
heading.appendChild(meta);
}
meta.textContent = source.label;
meta.onclick = (event) => {
event.preventDefault();
event.stopPropagation();
if (typeof source.onClick === "function") {
source.onClick();
}
};
}
function refreshInheritedSourceLabels() {
const groups = document.querySelectorAll(".scope-group-inherited");
groups.forEach((group) => {
const resolver = group._resolveInheritedSource;
if (!resolver) return;
const source = typeof resolver === "function" ? resolver() : resolver;
setInheritedSource(group, source);
});
}
function getTocLevel(link) {
if (link.closest(".toc-cards")) return 2;
if (link.closest(".toc-sub")) return 1;
return 0;
}
function isElementVisibleForHighlight(element) {
if (!element || typeof element.getBoundingClientRect !== "function") {
return false;
}
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
let node = element;
while (node) {
if (node.tagName === "DETAILS" && !node.open) {
const summary = node.querySelector(":scope > summary");
if (!summary || !summary.contains(element)) {
return false;
}
}
node = node.parentElement;
}
const style = window.getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden") {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.height <= 0 || rect.width <= 0) return false;
if (rect.bottom <= 0 || rect.top >= viewportHeight) return false;
if (rect.right <= 0 || rect.left >= viewportWidth) return false;
return true;
}
function resolveTocTarget(link) {
const selector = link.dataset.tocTargetSelector;
if (selector) {
const target = document.querySelector(selector);
if (target) return target;
}
const href = link.getAttribute("href");
if (href && href.startsWith("#") && href.length > 1) {
const target = document.querySelector(href);
if (target) return target;
}
return null;
}
function resolveTocHeader(target) {
if (!target) return null;
const summary = target.querySelector(":scope > summary.panel-summary");
if (summary) return summary;
const heading = target.querySelector(".panel-summary h3, .panel-summary h2");
if (heading) return heading.closest(".panel-summary") || heading;
return target;
}
function findVisibleTocLinkInAncestors(target) {
if (!target) return null;
let node = target;
let depth = 0;
while (node && depth < 3) {
const link = tocTargetMap.get(node);
if (link && isElementVisibleForHighlight(link)) {
return link;
}
node = node.parentElement?.closest("details") || null;
depth += 1;
}
return null;
}
function setActiveTocLink(link) {
if (activeTocLink === link) return;
if (activeTocLink) activeTocLink.classList.remove("toc-active");
activeTocLink = link || null;
if (activeTocLink) activeTocLink.classList.add("toc-active");
}
function updateTocHighlight() {
if (!tocTargets.length) {
setActiveTocLink(null);
return;
}
const visible = [];
tocTargets.forEach((entry) => {
const { link, header, level, order } = entry;
if (!header || !header.isConnected) return;
if (!isElementVisibleForHighlight(link)) return;
if (!isElementVisibleForHighlight(header)) return;
const rect = header.getBoundingClientRect();
visible.push({ link, level, order, top: rect.top, entry });
});
if (!visible.length) {
const bodyCandidates = [];
tocTargets.forEach((entry) => {
const { link, header, level, order } = entry;
if (!header || !header.isConnected) return;
if (!isElementVisibleForHighlight(header)) return;
const rect = header.getBoundingClientRect();
bodyCandidates.push({ link, level, order, top: rect.top, entry });
});
if (!bodyCandidates.length) {
setActiveTocLink(null);
return;
}
const maxLevel = Math.max(...bodyCandidates.map((item) => item.level));
const sameLevel = bodyCandidates.filter((item) => item.level === maxLevel);
sameLevel.sort((a, b) => {
if (a.top !== b.top) return a.top - b.top;
return a.order - b.order;
});
const candidate = sameLevel[0]?.entry;
if (!candidate) {
setActiveTocLink(null);
return;
}
const fallback = findVisibleTocLinkInAncestors(candidate.target);
setActiveTocLink(fallback);
return;
}
const maxLevel = Math.max(...visible.map((item) => item.level));
const sameLevel = visible.filter((item) => item.level === maxLevel);
sameLevel.sort((a, b) => {
if (a.top !== b.top) return a.top - b.top;
return a.order - b.order;
});
setActiveTocLink(sameLevel[0]?.link || null);
}
function scheduleTocHighlight() {
if (tocHighlightFrame) return;
tocHighlightFrame = requestAnimationFrame(() => {
tocHighlightFrame = null;
updateTocHighlight();
});
}
function refreshTocTargets() {
tocTargets = [];
tocTargetMap = new WeakMap();
const links = document.querySelectorAll(".toc-links a");
links.forEach((link, order) => {
const target = resolveTocTarget(link);
if (!target) return;
const header = resolveTocHeader(target);
if (!header) return;
tocTargetMap.set(target, link);
tocTargets.push({
link,
target,
header,
level: getTocLevel(link),
order
});
});
scheduleTocHighlight();
}
function buildScopeGroup(title, content, meta) {
const wrapper = document.createElement("div");
wrapper.className = "scope-group";
const heading = document.createElement("div");
heading.className = "scope-title hint-accent";
const titleSpan = document.createElement("span");
titleSpan.className = "scope-title-text";
titleSpan.textContent = title;
heading.appendChild(titleSpan);
wrapper.appendChild(heading);
wrapper.appendChild(content);
if (meta) {
setInheritedSource(wrapper, meta);
}
return wrapper;
}
function wireInheritedListHandlers(list, module) {
list.addEventListener("change", (event) => {
if (!event.target.classList.contains("inherited-toggle")) return;
if (module === "envs") {
updateTaskEnvOptions();
updateShortcutOptions();
} else if (module === "profiles") {
updateTaskProfileOptions();
updateShortcutOptions();
} else if (module === "tasks") {
updateShortcutOptions();
}
scheduleSidebarErrors();
});
}
function replaceInheritedList(list, module, parentItems, localContainer) {
const disabled = collectDisabledInherited(list);
const resolvedParents =
typeof parentItems === "function" ? parentItems() : parentItems;
const locals = collectLocalItemsForModule(module, localContainer);
const nextList = buildInheritedList(resolvedParents, locals, disabled);
nextList.dataset.module = module;
wireInheritedListHandlers(nextList, module);
list.replaceWith(nextList);
return nextList;
}
function collectLocalItemsForModule(module, container) {
if (!container) return [];
if (module === "envs") return collectEnvConfigs(container);
if (module === "profiles") return collectProfiles(container);
if (module === "tasks") return collectTasks(container);
if (module === "shortcuts") return collectShortcuts(container);
return [];
}
function buildScopedModuleSection({
title,
module,
parentItems,
localItems,
disabledNames,
localLabel,
localContainerClass,
buildCard,
newItemFactory,
cardOptions,
stateKey,
inheritedFrom
}) {
const details = document.createElement("details");
details.className = "panel sub-panel";
if (stateKey) {
details.dataset.stateKey = stateKey;
}
const summary = document.createElement("summary");
summary.className = "panel-summary";
const summaryRow = document.createElement("div");
summaryRow.className = "panel-summary-row";
const summaryLeft = document.createElement("div");
summaryLeft.className = "panel-summary-left";
const summaryRight = document.createElement("div");
summaryRight.className = "panel-summary-right";
const summaryTitle = document.createElement("h3");
summaryTitle.textContent = title;
summaryTitle.style.display = "inline";
summaryTitle.style.fontSize = "13px";
summaryTitle.style.fontWeight = "600";
summaryTitle.style.margin = "0";
summaryLeft.appendChild(summaryTitle);
summaryRow.appendChild(summaryLeft);
summaryRow.appendChild(summaryRight);
summary.appendChild(summaryRow);
details.appendChild(summary);
const body = document.createElement("div");
body.className = "panel-body";
const resolvedParents =
typeof parentItems === "function" ? parentItems() : parentItems;
let inheritedList = buildInheritedList(
resolvedParents,
localItems,
disabledNames
);
inheritedList.dataset.module = module;
wireInheritedListHandlers(inheritedList, module);
const refreshInherited = () => {
inheritedList = replaceInheritedList(
inheritedList,
module,
parentItems,
localContainer
);
};
const inheritedSource =
typeof inheritedFrom === "function" ? inheritedFrom() : inheritedFrom;
const inheritedGroup = buildScopeGroup(
"Inherited",
inheritedList,
inheritedSource
);
inheritedGroup.classList.add("scope-group-inherited");
if (inheritedFrom) {
inheritedGroup._resolveInheritedSource = inheritedFrom;
}
body.appendChild(inheritedGroup);
const localContainer = document.createElement("div");
localContainer.className = localContainerClass;
const items = Array.isArray(localItems) ? localItems : [];
for (const item of items) {
const options =
typeof cardOptions === "function" ? cardOptions() : cardOptions;
localContainer.appendChild(buildCard(item, localContainer, options));
}
const addBtn = document.createElement("button");
addBtn.type = "button";
addBtn.className = "accent";
addBtn.textContent = "Add";
addBtn.addEventListener("click", () => {
openDetails(details);
const newItem = newItemFactory(localContainer);
const options =
typeof cardOptions === "function" ? cardOptions() : cardOptions;
const newCard = buildCard(newItem, localContainer, options);
const first = localContainer.firstElementChild;
if (first) {
localContainer.insertBefore(newCard, first);
} else {
localContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
if (module === "envs") {
updateEnvApiOptions();
} else if (module === "profiles") {
updateTaskProfileOptions();
} else if (module === "tasks") {
updateShortcutOptions();
}
refreshInherited();
scheduleSidebarErrors();
});
summaryRight.appendChild(addBtn);
const localWrapper = document.createElement("div");
localWrapper.appendChild(localContainer);
body.appendChild(buildScopeGroup(localLabel, localWrapper));
const nameSelector = {
envs: ".env-config-name",
profiles: ".profile-name",
tasks: ".task-name",
shortcuts: ".shortcut-name"
}[module];
if (nameSelector) {
localContainer.addEventListener("input", (event) => {
if (event.target.matches(nameSelector)) {
refreshInherited();
}
});
}
localContainer.addEventListener("click", (event) => {
if (event.target.closest(".delete")) {
setTimeout(refreshInherited, 0);
}
});
refreshInherited();
details.appendChild(body);
registerDetail(details, details.open);
return { details, localContainer };
}
function collectDisabledInherited(listContainer) {
if (!listContainer) return [];
const disabled = [];
const items = listContainer.querySelectorAll(".inherited-item");
items.forEach((item) => {
if (item.dataset.overridden === "true") return;
const toggle = item.querySelector(".inherited-toggle");
if (toggle && !toggle.checked) {
disabled.push(item.dataset.key);
}
});
return disabled;
}
function listWorkspaceTargets() {
return [...workspacesContainer.querySelectorAll(".workspace-card")].map(
(card) => ({
id: card.dataset.id || "",
name: card.querySelector(".workspace-name")?.value || "Untitled Workspace"
})
);
}
function listSiteTargets() {
return [...sitesContainer.querySelectorAll(".site-card")].map((card) => ({
id: card.dataset.id || "",
name:
card.querySelector(".site-name")?.value ||
card.querySelector(".site-pattern")?.value ||
"Untitled Site"
}));
}
function fillTargetSelect(select, options, placeholder) {
select.innerHTML = "";
const initial = document.createElement("option");
initial.value = "";
initial.textContent = placeholder;
select.appendChild(initial);
for (const option of options) {
const opt = document.createElement("option");
opt.value = option.id;
opt.textContent = option.name;
select.appendChild(opt);
}
}
function getWorkspaceScopeData(workspaceCard) {
const globalEnvs = collectEnvConfigs();
const globalProfiles = collectProfiles();
const globalTasks = collectTasks();
const globalShortcuts = collectShortcuts();
const envs = collectEnvConfigs(
workspaceCard.querySelector(".workspace-envs")
);
const profiles = collectProfiles(
workspaceCard.querySelector(".workspace-profiles")
);
const tasks = collectTasks(workspaceCard.querySelector(".workspace-tasks"));
const shortcuts = collectShortcuts(
workspaceCard.querySelector(".workspace-shortcuts")
);
const envDisabled = collectDisabledInherited(
workspaceCard.querySelector('.inherited-list[data-module="envs"]')
);
const profileDisabled = collectDisabledInherited(
workspaceCard.querySelector('.inherited-list[data-module="profiles"]')
);
const taskDisabled = collectDisabledInherited(
workspaceCard.querySelector('.inherited-list[data-module="tasks"]')
);
const shortcutDisabled = collectDisabledInherited(
workspaceCard.querySelector('.inherited-list[data-module="shortcuts"]')
);
const envScope = resolveScopedItems(globalEnvs, envs, envDisabled);
const profileScope = resolveScopedItems(
globalProfiles,
profiles,
profileDisabled
);
const taskScope = resolveScopedItems(globalTasks, tasks, taskDisabled);
const shortcutScope = resolveScopedItems(
globalShortcuts,
shortcuts,
shortcutDisabled
);
return {
envs: envScope.effective,
profiles: profileScope.effective,
tasks: taskScope.effective,
shortcuts: shortcutScope.effective
};
}
function getSiteScopeData(siteCard) {
const workspaceId = siteCard.querySelector(".site-workspace")?.value || "global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
const workspaceScope = workspaceCard
? getWorkspaceScopeData(workspaceCard)
: {
envs: collectEnvConfigs(),
profiles: collectProfiles(),
tasks: collectTasks(),
shortcuts: collectShortcuts()
};
const envs = collectEnvConfigs(siteCard.querySelector(".site-envs"));
const profiles = collectProfiles(siteCard.querySelector(".site-profiles"));
const tasks = collectTasks(siteCard.querySelector(".site-tasks"));
const shortcuts = collectShortcuts(siteCard.querySelector(".site-shortcuts"));
const envDisabled = collectDisabledInherited(
siteCard.querySelector('.inherited-list[data-module="envs"]')
);
const profileDisabled = collectDisabledInherited(
siteCard.querySelector('.inherited-list[data-module="profiles"]')
);
const taskDisabled = collectDisabledInherited(
siteCard.querySelector('.inherited-list[data-module="tasks"]')
);
const shortcutDisabled = collectDisabledInherited(
siteCard.querySelector('.inherited-list[data-module="shortcuts"]')
);
const envScope = resolveScopedItems(
workspaceScope.envs,
envs,
envDisabled
);
const profileScope = resolveScopedItems(
workspaceScope.profiles,
profiles,
profileDisabled
);
const taskScope = resolveScopedItems(
workspaceScope.tasks,
tasks,
taskDisabled
);
const shortcutScope = resolveScopedItems(
workspaceScope.shortcuts || [],
shortcuts,
shortcutDisabled
);
return {
envs: envScope.effective,
profiles: profileScope.effective,
tasks: taskScope.effective,
shortcuts: shortcutScope.effective
};
}
function buildDuplicateCard(module, source, container, options) {
const nameValue = source.name || "Untitled";
if (module === "envs") {
const names = collectNames(container, ".env-config-name");
const copy = {
...source,
id: newEnvConfigId(),
name: ensureUniqueName(`${nameValue} Copy`, names),
enabled: source.enabled !== false
};
return buildEnvConfigCard(copy, container);
}
if (module === "profiles") {
const names = collectNames(container, ".profile-name");
const copy = {
...source,
id: newProfileId(),
name: ensureUniqueName(`${nameValue} Copy`, names),
enabled: source.enabled !== false
};
return buildProfileCard(copy, container);
}
if (module === "tasks") {
const names = collectNames(container, ".task-name");
const envs = options?.envs || [];
const profiles = options?.profiles || [];
const copy = {
...source,
id: newTaskId(),
name: ensureUniqueName(`${nameValue} Copy`, names),
enabled: source.enabled !== false,
defaultEnvId: envs.some((env) => env.id === source.defaultEnvId)
? source.defaultEnvId
: envs[0]?.id || "",
defaultProfileId: profiles.some(
(profile) => profile.id === source.defaultProfileId
)
? source.defaultProfileId
: profiles[0]?.id || ""
};
return buildTaskCard(copy, container, { envs, profiles });
}
if (module === "shortcuts") {
const names = collectNames(container, ".shortcut-name");
const envs = options?.envs || [];
const profiles = options?.profiles || [];
const tasks = options?.tasks || [];
const copy = {
...source,
id: newShortcutId(),
name: ensureUniqueName(`${nameValue} Copy`, names),
enabled: source.enabled !== false,
envId: envs.some((env) => env.id === source.envId)
? source.envId
: envs[0]?.id || "",
profileId: profiles.some((profile) => profile.id === source.profileId)
? source.profileId
: profiles[0]?.id || "",
taskId: tasks.some((task) => task.id === source.taskId)
? source.taskId
: tasks[0]?.id || ""
};
return buildShortcutCard(copy, container, { envs, profiles, tasks });
}
return null;
}
function insertCardAtTop(container, card) {
if (!container || !card) return;
const first = container.firstElementChild;
if (first) {
container.insertBefore(card, first);
} else {
container.appendChild(card);
}
}
function openScopedSection(scopeCard, prefix, module) {
if (!scopeCard || !module) return;
const id = scopeCard.dataset.id;
if (!id) return;
const section = scopeCard.querySelector(
`details[data-state-key="${prefix}:${id}:${module}"]`
);
if (section) openDetails(section);
}
function refreshAfterModuleChange(module) {
if (module === "envs") {
updateEnvApiOptions();
updateTaskEnvOptions();
} else if (module === "profiles") {
updateTaskProfileOptions();
} else if (module === "tasks") {
updateShortcutOptions();
} else if (module === "shortcuts") {
updateShortcutOptions();
}
scheduleSidebarErrors();
}
const MOTION_DURATION_MS = 420;
const MOTION_EASING = "cubic-bezier(0.2, 0, 0.2, 1)";
let activeScrollToken = 0;
function cubicBezierAtTime(t, p1x, p1y, p2x, p2y) {
if (p1x === p1y && p2x === p2y) return t;
const cx = 3 * p1x;
const bx = 3 * (p2x - p1x) - cx;
const ax = 1 - cx - bx;
const cy = 3 * p1y;
const by = 3 * (p2y - p1y) - cy;
const ay = 1 - cy - by;
const sampleX = (tVal) => ((ax * tVal + bx) * tVal + cx) * tVal;
const sampleY = (tVal) => ((ay * tVal + by) * tVal + cy) * tVal;
let start = 0;
let end = 1;
let current = t;
for (let i = 0; i < 20; i += 1) {
const x = sampleX(current);
const delta = x - t;
if (Math.abs(delta) < 1e-4) break;
if (delta > 0) {
end = current;
current = (start + current) / 2;
} else {
start = current;
current = (current + end) / 2;
}
}
return sampleY(current);
}
function motionEase(t) {
return cubicBezierAtTime(t, 0.2, 0, 0.2, 1);
}
function animateScrollTo(target) {
const start = window.scrollY || 0;
const delta = target - start;
if (!delta) return;
const prefersReduced =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReduced) {
window.scrollTo(0, target);
return;
}
const token = (activeScrollToken += 1);
const startTime = performance.now();
const step = (now) => {
if (token !== activeScrollToken) return;
const elapsed = now - startTime;
const progress = Math.min(elapsed / MOTION_DURATION_MS, 1);
const eased = motionEase(progress);
window.scrollTo(0, start + delta * eased);
if (progress < 1) {
requestAnimationFrame(step);
}
};
requestAnimationFrame(step);
}
function scrollCardToCenter(card) {
if (!card || typeof card.getBoundingClientRect !== "function") return;
const rect = card.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const target =
rect.top + window.scrollY - (viewportHeight / 2 - rect.height / 2);
animateScrollTo(Math.max(0, target));
}
function updateModuleControls(module, container) {
if (!container) return;
if (module === "envs") updateEnvControls(container);
else if (module === "profiles") updateProfileControls(container);
else if (module === "tasks") updateTaskControls(container);
else if (module === "shortcuts") updateShortcutControls(container);
}
function isElementInViewport(element) {
if (!element || typeof element.getBoundingClientRect !== "function") return false;
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.bottom > 0 &&
rect.top < viewportHeight &&
rect.right > 0 &&
rect.left < viewportWidth
);
}
function runWhenVisible(element, callback, options = {}) {
if (!element || typeof callback !== "function") return;
const { scrollToCenter = false } = options;
if (isElementInViewport(element)) {
callback();
return;
}
if (scrollToCenter) {
scrollCardToCenter(element);
requestAnimationFrame(() => {
if (!element.isConnected) return;
callback();
});
return;
}
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
observer.disconnect();
callback();
break;
}
}
}, { threshold: 0.1 });
observer.observe(element);
}
function applyFlipAnimation(card, deltaX, deltaY) {
if (!card) return;
if (!deltaX && !deltaY) return;
card.style.transition = "none";
card.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
card.style.willChange = "transform";
card.getBoundingClientRect();
requestAnimationFrame(() => {
card.style.transition = `transform ${MOTION_DURATION_MS}ms ${MOTION_EASING}`;
card.style.transform = "translate(0, 0)";
const cleanup = () => {
card.style.transition = "";
card.style.transform = "";
card.style.willChange = "";
card.removeEventListener("transitionend", cleanup);
};
card.addEventListener("transitionend", cleanup);
});
}
function animateDuplicateFromRect(card, sourceRect) {
if (!card || !sourceRect) return;
const prefersReduced =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReduced) return;
const sourceScrollX = window.scrollX || 0;
const sourceScrollY = window.scrollY || 0;
const sourcePage = {
left: sourceRect.left + sourceScrollX,
top: sourceRect.top + sourceScrollY
};
requestAnimationFrame(() => {
runWhenVisible(
card,
() => {
if (!card.isConnected) return;
const destRect = card.getBoundingClientRect();
const currentScrollX = window.scrollX || 0;
const currentScrollY = window.scrollY || 0;
const sourceViewport = {
left: sourcePage.left - currentScrollX,
top: sourcePage.top - currentScrollY
};
const deltaX = sourceViewport.left - destRect.left;
const deltaY = sourceViewport.top - destRect.top;
applyFlipAnimation(card, deltaX, deltaY);
},
{ scrollToCenter: true }
);
});
}
function animateCardMove(card, applyMove, options = {}) {
if (!card || typeof applyMove !== "function") return;
const firstRect = card.getBoundingClientRect();
applyMove();
const lastRect = card.getBoundingClientRect();
const deltaX = firstRect.left - lastRect.left;
const deltaY = firstRect.top - lastRect.top;
const prefersReduced =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReduced || (!deltaX && !deltaY)) {
return;
}
runWhenVisible(card, () => applyFlipAnimation(card, deltaX, deltaY), {
scrollToCenter: Boolean(options.scrollToCenter)
});
}
function moveCardToContainer(card, container) {
if (!card || !container) return;
animateCardMove(card, () => insertCardAtTop(container, card), {
scrollToCenter: true
});
}
function moveCardToWorkspace(module, card, workspaceId) {
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
if (!workspaceCard) return;
const container = workspaceCard.querySelector(`.workspace-${module}`);
if (!container) return;
const origin = card.parentElement;
moveCardToContainer(card, container);
if (origin && origin !== container) {
updateModuleControls(module, origin);
}
updateModuleControls(module, container);
openDetailsChain(workspaceCard);
openScopedSection(workspaceCard, "workspace", module);
openDetails(card);
refreshAfterModuleChange(module);
}
function moveCardToSite(module, card, siteId) {
const siteCard = document.querySelector(`.site-card[data-id="${siteId}"]`);
if (!siteCard) return;
const container = siteCard.querySelector(`.site-${module}`);
if (!container) return;
const origin = card.parentElement;
moveCardToContainer(card, container);
if (origin && origin !== container) {
updateModuleControls(module, origin);
}
updateModuleControls(module, container);
openDetailsChain(siteCard);
openScopedSection(siteCard, "site", module);
openDetails(card);
refreshAfterModuleChange(module);
}
function getGlobalContainerForModule(module) {
if (module === "envs") return envConfigsContainer;
if (module === "profiles") return profilesContainer;
if (module === "tasks") return tasksContainer;
if (module === "shortcuts") return shortcutsContainer;
return null;
}
function getGlobalScopeForModule(module) {
if (module === "tasks") {
return {
envs: collectEnvConfigs().filter((env) => isEnabled(env.enabled)),
profiles: collectProfiles().filter((profile) => isEnabled(profile.enabled))
};
}
if (module === "shortcuts") {
return {
envs: collectEnvConfigs().filter((env) => isEnabled(env.enabled)),
profiles: collectProfiles().filter((profile) => isEnabled(profile.enabled)),
tasks: collectTasks().filter((task) => isEnabled(task.enabled))
};
}
return null;
}
function openGlobalSectionForModule(module) {
const globalPanel = document.getElementById("global-config-panel");
const sectionId = {
envs: "environment-panel",
profiles: "profiles-panel",
tasks: "tasks-panel",
shortcuts: "shortcuts-panel"
}[module];
if (sectionId) {
const section = document.getElementById(sectionId);
if (section) {
openDetailsChain(section);
openDetails(section);
return;
}
}
if (globalPanel) {
openDetailsChain(globalPanel);
openDetails(globalPanel);
}
}
function focusGlobalModule(module) {
const sectionId = {
envs: "environment-panel",
profiles: "profiles-panel",
tasks: "tasks-panel",
shortcuts: "shortcuts-panel"
}[module];
if (sectionId) {
const section = document.getElementById(sectionId);
if (section) {
openDetailsChain(section);
openDetails(section);
centerCardInView(section);
return;
}
}
const globalPanel = document.getElementById("global-config-panel");
if (globalPanel) {
openDetailsChain(globalPanel);
openDetails(globalPanel);
centerCardInView(globalPanel);
}
}
function focusGlobalApiConfigSection() {
const section = document.getElementById("api-panel");
if (section) {
openDetailsChain(section);
openDetails(section);
centerCardInView(section);
return;
}
const globalPanel = document.getElementById("global-config-panel");
if (globalPanel) {
openDetailsChain(globalPanel);
openDetails(globalPanel);
centerCardInView(globalPanel);
}
}
function focusWorkspaceModule(workspaceId, module) {
if (!workspaceId || workspaceId === "global") {
focusGlobalModule(module);
return;
}
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
if (!workspaceCard) return;
openDetailsChain(workspaceCard);
openScopedSection(workspaceCard, "workspace", module);
const section = workspaceCard.querySelector(
`details[data-state-key="workspace:${workspaceId}:${module}"]`
);
if (section) {
openDetails(section);
centerCardInView(section);
return;
}
centerCardInView(workspaceCard);
}
function getWorkspaceNameById(workspaceId) {
if (!workspaceId || workspaceId === "global") return "Global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
const name =
workspaceCard?.querySelector(".workspace-name")?.value?.trim() || "";
return name || "Untitled Workspace";
}
function moveCardToGlobal(module, card) {
const container = getGlobalContainerForModule(module);
if (!container) return;
const origin = card.parentElement;
moveCardToContainer(card, container);
if (origin && origin !== container) {
updateModuleControls(module, origin);
}
updateModuleControls(module, container);
openGlobalSectionForModule(module);
openDetails(card);
refreshAfterModuleChange(module);
}
function duplicateToWorkspace(module, source, workspaceId, sourceRect) {
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${workspaceId}"]`
);
if (!workspaceCard) return;
const container = workspaceCard.querySelector(`.workspace-${module}`);
if (!container) return;
const scope = getWorkspaceScopeData(workspaceCard);
const card = buildDuplicateCard(module, source, container, scope);
if (card) {
insertCardAtTop(container, card);
openDetailsChain(workspaceCard);
openScopedSection(workspaceCard, "workspace", module);
openDetails(card);
animateDuplicateFromRect(card, sourceRect);
updateModuleControls(module, container);
refreshAfterModuleChange(module);
centerCardInViewAfterLayout(card);
}
}
function duplicateToSite(module, source, siteId, sourceRect) {
const siteCard = document.querySelector(`.site-card[data-id="${siteId}"]`);
if (!siteCard) return;
const container = siteCard.querySelector(`.site-${module}`);
if (!container) return;
const scope = getSiteScopeData(siteCard);
const card = buildDuplicateCard(module, source, container, scope);
if (card) {
insertCardAtTop(container, card);
openDetailsChain(siteCard);
openScopedSection(siteCard, "site", module);
openDetails(card);
animateDuplicateFromRect(card, sourceRect);
updateModuleControls(module, container);
refreshAfterModuleChange(module);
centerCardInViewAfterLayout(card);
}
}
function duplicateToGlobal(module, source, sourceRect) {
const container = getGlobalContainerForModule(module);
if (!container) return;
const scope = getGlobalScopeForModule(module);
const card = buildDuplicateCard(module, source, container, scope);
if (card) {
insertCardAtTop(container, card);
openGlobalSectionForModule(module);
openDetails(card);
animateDuplicateFromRect(card, sourceRect);
updateModuleControls(module, container);
refreshAfterModuleChange(module);
centerCardInViewAfterLayout(card);
}
}
function buildScopedActionControls(label, handlers = {}) {
const wrapper = document.createElement("div");
wrapper.className = "dup-controls";
const { onHere, onGlobal, onWorkspace, onSite } = handlers;
const select = document.createElement("select");
select.className = "dup-select";
select.addEventListener("click", (event) => event.stopPropagation());
select.addEventListener("mousedown", (event) => event.stopPropagation());
const placeholderValue = "__placeholder__";
let menuMode = "root";
let outsideHandler = null;
const setOptions = (options, placeholderLabel) => {
select.innerHTML = "";
if (placeholderLabel) {
const placeholder = document.createElement("option");
placeholder.value = placeholderValue;
placeholder.textContent = placeholderLabel;
select.appendChild(placeholder);
}
for (const option of options) {
const entry = document.createElement("option");
entry.value = option.value;
entry.textContent = option.label;
if (option.disabled) entry.disabled = true;
if (option.kind === "cancel") {
entry.className = "dup-option-cancel";
entry.style.color = "#c0392b";
entry.style.fontWeight = "600";
}
select.appendChild(entry);
}
if (placeholderLabel) {
select.value = placeholderValue;
}
};
const attachOutsideHandler = () => {
if (outsideHandler) return;
outsideHandler = (event) => {
if (wrapper.contains(event.target)) return;
hideMenu();
};
document.addEventListener("mousedown", outsideHandler);
document.addEventListener("touchstart", outsideHandler);
};
const detachOutsideHandler = () => {
if (!outsideHandler) return;
document.removeEventListener("mousedown", outsideHandler);
document.removeEventListener("touchstart", outsideHandler);
outsideHandler = null;
};
const showMenu = (mode) => {
menuMode = mode;
if (mode === "root") {
setOptions([
{ value: "here", label: "Here" },
{ value: "global", label: "Global" },
{ value: "workspace", label: "Workspace" },
{ value: "site", label: "Site" },
{ value: "cancel", label: "Cancel", kind: "cancel" }
], label);
detachOutsideHandler();
} else if (mode === "workspace") {
const targets = listWorkspaceTargets();
const options = [
{ value: "back", label: "Back" },
{ value: "cancel", label: "Cancel", kind: "cancel" }
];
if (!targets.length) {
options.push({ value: "", label: "No workspaces", disabled: true });
} else {
targets.forEach((target) => {
options.push({ value: target.id, label: target.name });
});
}
setOptions(options, "Select Destination");
attachOutsideHandler();
} else if (mode === "site") {
const targets = listSiteTargets();
const options = [
{ value: "back", label: "Back" },
{ value: "cancel", label: "Cancel", kind: "cancel" }
];
if (!targets.length) {
options.push({ value: "", label: "No sites", disabled: true });
} else {
targets.forEach((target) => {
options.push({ value: target.id, label: target.name });
});
}
setOptions(options, "Select Destination");
attachOutsideHandler();
}
};
const resetMenu = () => {
menuMode = "root";
showMenu("root");
};
showMenu("root");
select.addEventListener("change", () => {
const value = select.value;
if (!value || value === placeholderValue) return;
if (menuMode === "root") {
if (value === "cancel") {
resetMenu();
return;
}
if (value === "here") {
if (typeof onHere === "function") {
onHere();
}
resetMenu();
return;
}
if (value === "global") {
if (typeof onGlobal === "function") {
onGlobal();
}
resetMenu();
return;
}
if (value === "workspace" || value === "site") {
showMenu(value);
}
return;
}
if (menuMode === "workspace") {
if (value === "cancel") {
resetMenu();
return;
}
if (value === "back") {
showMenu("root");
return;
}
if (typeof onWorkspace === "function") {
onWorkspace(value);
}
resetMenu();
return;
}
if (menuMode === "site") {
if (value === "cancel") {
resetMenu();
return;
}
if (value === "back") {
showMenu("root");
return;
}
if (typeof onSite === "function") {
onSite(value);
}
resetMenu();
}
});
select.addEventListener("blur", () => {
resetMenu();
});
wrapper.appendChild(select);
return wrapper;
}
function buildDuplicateControls(module, getSourceData, options = {}) {
const { onHere, sourceCard } = options;
const getSourceRect = () =>
sourceCard && typeof sourceCard.getBoundingClientRect === "function"
? sourceCard.getBoundingClientRect()
: null;
return buildScopedActionControls("Duplicate", {
onHere,
onGlobal: () => duplicateToGlobal(module, getSourceData(), getSourceRect()),
onWorkspace: (workspaceId) =>
duplicateToWorkspace(module, getSourceData(), workspaceId, getSourceRect()),
onSite: (siteId) =>
duplicateToSite(module, getSourceData(), siteId, getSourceRect())
});
}
function buildMoveControls(module, card, container) {
return buildScopedActionControls("Move", {
onHere: () => {
const origin = card.parentElement;
moveCardToContainer(card, container);
if (origin) updateModuleControls(module, origin);
updateModuleControls(module, container);
openDetails(card);
refreshAfterModuleChange(module);
},
onGlobal: () => moveCardToGlobal(module, card),
onWorkspace: (workspaceId) => moveCardToWorkspace(module, card, workspaceId),
onSite: (siteId) => moveCardToSite(module, card, siteId)
});
}
function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) {
const card = document.createElement("details");
card.className = "workspace-card";
card.dataset.id = ws.id || newWorkspaceId();
card.dataset.stateKey = `workspace:${card.dataset.id}`;
const nameField = document.createElement("div");
nameField.className = "field";
nameField.style.flex = "1";
const nameLabel = document.createElement("label");
nameLabel.textContent = "Workspace name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = ws.name || "";
nameInput.className = "workspace-name";
nameInput.placeholder = "Workspace Name";
nameInput.addEventListener("input", () => {
updateToc(collectWorkspaces(), collectSites());
scheduleSidebarErrors();
});
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const { body, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled Workspace",
{ subPanel: false }
);
const header = document.createElement("div");
header.className = "row workspace-header";
header.style.alignItems = "flex-end";
header.appendChild(nameField);
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
if (confirm(`Delete workspace "${ws.name}"? All items will move to global.`)) {
card.remove();
scheduleSidebarErrors();
updateAppearanceInheritanceIndicators();
updateToc(collectWorkspaces(), collectSites());
}
});
summaryRight.appendChild(deleteBtn);
body.appendChild(header);
const appearanceSection = buildAppearanceSection(
{
theme: ws.theme || "inherit",
toolbarPosition: ws.toolbarPosition || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
ws.alwaysUseDefaultEnvProfile
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
ws.emptyToolbarBehavior
)
},
{ stateKey: `workspace:${card.dataset.id}:appearance` }
);
body.appendChild(appearanceSection);
const disabledInherited = ws.disabledInherited || {};
const globalApiConfigs = collectApiConfigs();
const apiConfigSection = document.createElement("details");
apiConfigSection.className = "panel sub-panel";
apiConfigSection.dataset.stateKey = `workspace:${card.dataset.id}:apiConfigs`;
const apiSummary = document.createElement("summary");
apiSummary.className = "panel-summary";
const apiSummaryRow = document.createElement("div");
apiSummaryRow.className = "panel-summary-row";
const apiSummaryLeft = document.createElement("div");
apiSummaryLeft.className = "panel-summary-left";
const apiSummaryRight = document.createElement("div");
apiSummaryRight.className = "panel-summary-right";
const apiSummaryTitle = document.createElement("h3");
apiSummaryTitle.textContent = "API Configurations";
apiSummaryTitle.style.display = "inline";
apiSummaryTitle.style.fontSize = "13px";
apiSummaryTitle.style.fontWeight = "600";
apiSummaryTitle.style.margin = "0";
const apiSummaryLink = document.createElement("button");
apiSummaryLink.type = "button";
apiSummaryLink.className = "panel-meta-link hint-accent";
apiSummaryLink.textContent = "Global";
apiSummaryLink.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
focusGlobalApiConfigSection();
});
apiSummaryLeft.appendChild(apiSummaryTitle);
apiSummaryLeft.appendChild(apiSummaryLink);
apiSummaryRow.appendChild(apiSummaryLeft);
apiSummaryRow.appendChild(apiSummaryRight);
apiSummary.appendChild(apiSummaryRow);
apiConfigSection.appendChild(apiSummary);
const apiBody = document.createElement("div");
apiBody.className = "panel-body";
const apiList = buildApiConfigToggleList(
globalApiConfigs,
disabledInherited.apiConfigs || []
);
apiList.dataset.module = "apiConfigs";
apiBody.appendChild(apiList);
apiConfigSection.appendChild(apiBody);
registerDetail(apiConfigSection, apiConfigSection.open);
body.appendChild(apiConfigSection);
const envSection = buildScopedModuleSection({
title: "Environments",
module: "envs",
parentItems: () => collectEnvConfigs(),
localItems: ws.envConfigs || [],
disabledNames: disabledInherited.envs,
localLabel: "Workspace-specific",
localContainerClass: "workspace-envs",
buildCard: buildEnvConfigCard,
stateKey: `workspace:${card.dataset.id}:envs`,
inheritedFrom: () => ({
label: "Global",
onClick: () => focusGlobalModule("envs")
}),
newItemFactory: (container) => ({
id: newEnvConfigId(),
name: buildUniqueNumberedName(
"New Environment",
collectNames(container, ".env-config-name")
),
apiConfigId: getWorkspaceApiConfigs(card)[0]?.id || "",
systemPrompt: DEFAULT_SYSTEM_PROMPT,
enabled: true
})
});
body.appendChild(envSection.details);
const profileSection = buildScopedModuleSection({
title: "Profiles",
module: "profiles",
parentItems: () => collectProfiles(),
localItems: ws.profiles || [],
disabledNames: disabledInherited.profiles,
localLabel: "Workspace-specific",
localContainerClass: "workspace-profiles",
buildCard: buildProfileCard,
stateKey: `workspace:${card.dataset.id}:profiles`,
inheritedFrom: () => ({
label: "Global",
onClick: () => focusGlobalModule("profiles")
}),
newItemFactory: (container) => ({
id: newProfileId(),
name: buildUniqueNumberedName(
"New Profile",
collectNames(container, ".profile-name")
),
text: "",
enabled: true
})
});
body.appendChild(profileSection.details);
const taskSection = buildScopedModuleSection({
title: "Tasks",
module: "tasks",
parentItems: () => collectTasks(),
localItems: ws.tasks || [],
disabledNames: disabledInherited.tasks,
localLabel: "Workspace-specific",
localContainerClass: "workspace-tasks",
buildCard: buildTaskCard,
stateKey: `workspace:${card.dataset.id}:tasks`,
inheritedFrom: () => ({
label: "Global",
onClick: () => focusGlobalModule("tasks")
}),
cardOptions: () => {
const scope = getWorkspaceScopeData(card);
return { envs: scope.envs, profiles: scope.profiles };
},
newItemFactory: (container) => {
const scope = getWorkspaceScopeData(card);
return {
id: newTaskId(),
name: buildUniqueNumberedName(
"New Task",
collectNames(container, ".task-name")
),
text: "",
defaultEnvId: scope.envs[0]?.id || "",
defaultProfileId: scope.profiles[0]?.id || "",
enabled: true
};
}
});
body.appendChild(taskSection.details);
const shortcutSection = buildScopedModuleSection({
title: "Toolbar Shortcuts",
module: "shortcuts",
parentItems: () => collectShortcuts(),
localItems: ws.shortcuts || [],
disabledNames: disabledInherited.shortcuts,
localLabel: "Workspace-specific",
localContainerClass: "workspace-shortcuts",
buildCard: buildShortcutCard,
stateKey: `workspace:${card.dataset.id}:shortcuts`,
inheritedFrom: () => ({
label: "Global",
onClick: () => focusGlobalModule("shortcuts")
}),
cardOptions: () => {
const scope = getWorkspaceScopeData(card);
return { envs: scope.envs, profiles: scope.profiles, tasks: scope.tasks };
},
newItemFactory: (container) => {
const scope = getWorkspaceScopeData(card);
return {
id: newShortcutId(),
name: buildUniqueNumberedName(
"New Shortcut",
collectNames(container, ".shortcut-name")
),
envId: scope.envs[0]?.id || "",
profileId: scope.profiles[0]?.id || "",
taskId: scope.tasks[0]?.id || "",
enabled: true
};
}
});
body.appendChild(shortcutSection.details);
const sitesSection = document.createElement("details");
sitesSection.className = "panel sub-panel";
sitesSection.dataset.stateKey = `workspace:${card.dataset.id}:sites`;
const sitesSummary = document.createElement("summary");
sitesSummary.className = "panel-summary";
const sitesSummaryRow = document.createElement("div");
sitesSummaryRow.className = "panel-summary-row";
const sitesSummaryLeft = document.createElement("div");
sitesSummaryLeft.className = "panel-summary-left";
const sitesSummaryRight = document.createElement("div");
sitesSummaryRight.className = "panel-summary-right";
const sitesSummaryTitle = document.createElement("h3");
sitesSummaryTitle.textContent = "Sites";
sitesSummaryTitle.style.display = "inline";
sitesSummaryTitle.style.fontSize = "13px";
sitesSummaryTitle.style.fontWeight = "600";
sitesSummaryTitle.style.margin = "0";
sitesSummaryLeft.appendChild(sitesSummaryTitle);
sitesSummaryRow.appendChild(sitesSummaryLeft);
sitesSummaryRow.appendChild(sitesSummaryRight);
sitesSummary.appendChild(sitesSummaryRow);
sitesSection.appendChild(sitesSummary);
const sitesBody = document.createElement("div");
sitesBody.className = "panel-body";
const siteList = document.createElement("div");
siteList.className = "sites-list workspace-sites-list";
siteList.dataset.workspaceId = card.dataset.id;
renderWorkspaceSitesList(siteList, card.dataset.id, allSites);
sitesBody.appendChild(siteList);
sitesSection.appendChild(sitesBody);
registerDetail(sitesSection, sitesSection.open);
body.appendChild(sitesSection);
return card;
}
function collectSites() {
const cards = [...sitesContainer.querySelectorAll(".site-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".site-name");
const patternInput = card.querySelector(".site-pattern");
const workspaceSelect = card.querySelector(".site-workspace");
const extractInput = card.querySelector(".site-extract-selector");
const parsedTarget = parseExtractionTargetInput(extractInput?.value || "");
const themeSelect = card.querySelector(".appearance-theme");
const toolbarSelect = card.querySelector(".appearance-toolbar-position");
const defaultEnvProfileSelect = card.querySelector(
".appearance-default-env-profile"
);
const emptyToolbarSelect = card.querySelector(".appearance-empty-toolbar");
const envsContainer = card.querySelector(".site-envs");
const profilesContainer = card.querySelector(".site-profiles");
const tasksContainer = card.querySelector(".site-tasks");
const shortcutsContainer = card.querySelector(".site-shortcuts");
const envsInherited = card.querySelector('.inherited-list[data-module="envs"]');
const profilesInherited = card.querySelector('.inherited-list[data-module="profiles"]');
const tasksInherited = card.querySelector('.inherited-list[data-module="tasks"]');
const shortcutsInherited = card.querySelector('.inherited-list[data-module="shortcuts"]');
const apiConfigsInherited = card.querySelector('.inherited-list[data-module="apiConfigs"]');
return {
id: card.dataset.id || newSiteId(),
name: (nameInput?.value || "").trim(),
urlPattern: (patternInput?.value || "").trim(),
workspaceId: workspaceSelect?.value || "global",
extractTarget: parsedTarget.target,
theme: themeSelect?.value || "inherit",
toolbarPosition: toolbarSelect?.value || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
defaultEnvProfileSelect?.value
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
emptyToolbarSelect?.value
),
envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [],
profiles: profilesContainer ? collectProfiles(profilesContainer) : [],
tasks: tasksContainer ? collectTasks(tasksContainer) : [],
shortcuts: shortcutsContainer ? collectShortcuts(shortcutsContainer) : [],
disabledInherited: {
envs: collectDisabledInherited(envsInherited),
profiles: collectDisabledInherited(profilesInherited),
tasks: collectDisabledInherited(tasksInherited),
shortcuts: collectDisabledInherited(shortcutsInherited),
apiConfigs: collectDisabledInherited(apiConfigsInherited)
}
};
});
}
function buildSiteCard(site, allWorkspaces = []) {
const card = document.createElement("details");
card.className = "site-card";
card.dataset.id = site.id || newSiteId();
card.dataset.stateKey = `site:${card.dataset.id}`;
const row = document.createElement("div");
row.className = "row site-header";
row.style.alignItems = "flex-end";
const nameField = document.createElement("div");
nameField.className = "field";
nameField.style.flex = "3";
const nameLabel = document.createElement("label");
nameLabel.textContent = "Site name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = site.name || "";
nameInput.className = "site-name";
nameInput.placeholder = "Site Name";
nameInput.addEventListener("input", () => {
updateToc(collectWorkspaces(), collectSites());
scheduleSidebarErrors();
});
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const { body, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled Site",
{ subPanel: false }
);
const patternField = document.createElement("div");
patternField.className = "field";
patternField.style.flex = "3";
const patternLabel = document.createElement("label");
patternLabel.textContent = "URL Pattern";
const patternInput = document.createElement("input");
patternInput.type = "text";
patternInput.value = site.urlPattern || "";
patternInput.className = "site-pattern";
patternInput.placeholder = "example.com/*";
patternInput.addEventListener("input", () => {
updateToc(collectWorkspaces(), collectSites());
scheduleSidebarErrors();
});
patternField.appendChild(patternLabel);
patternField.appendChild(patternInput);
const wsField = document.createElement("div");
wsField.className = "field";
wsField.style.flex = "2";
const wsLabel = document.createElement("label");
wsLabel.textContent = "Workspace";
const wsSelect = document.createElement("select");
wsSelect.className = "site-workspace";
const globalOpt = document.createElement("option");
globalOpt.value = "global";
globalOpt.textContent = "Global";
wsSelect.appendChild(globalOpt);
for (const ws of allWorkspaces) {
const opt = document.createElement("option");
opt.value = ws.id;
opt.textContent = ws.name || "Untitled Workspace";
wsSelect.appendChild(opt);
}
wsSelect.value = site.workspaceId || "global";
wsField.appendChild(wsLabel);
wsField.appendChild(wsSelect);
const getSiteInheritedSource = (module) => () => {
const workspaceId = wsSelect.value || "global";
return {
label: getWorkspaceNameById(workspaceId),
onClick: () => focusWorkspaceModule(workspaceId, module)
};
};
wsSelect.addEventListener("change", () => {
const currentSites = collectSites();
const current = currentSites.find((entry) => entry.id === card.dataset.id);
if (!current) return;
const refreshed = {
...current,
workspaceId: wsSelect.value || "global",
disabledInherited: normalizeDisabledInherited()
};
const replacement = buildSiteCard(refreshed, collectWorkspaces());
card.replaceWith(replacement);
scheduleSidebarErrors();
updateEnvApiOptions();
updateTaskEnvOptions();
updateTaskProfileOptions();
updateShortcutOptions();
updateToc(collectWorkspaces(), collectSites());
});
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
scheduleSidebarErrors();
updateToc(collectWorkspaces(), collectSites());
});
row.appendChild(nameField);
row.appendChild(patternField);
row.appendChild(wsField);
summaryRight.appendChild(deleteBtn);
body.appendChild(row);
const extractField = document.createElement("div");
extractField.className = "field";
const extractLabel = document.createElement("label");
extractLabel.textContent = "Site Text Selector";
const extractInput = document.createElement("input");
extractInput.type = "text";
extractInput.value = serializeExtractionTarget(site.extractTarget);
extractInput.className = "site-extract-selector";
extractInput.placeholder = "body";
extractInput.addEventListener("input", () => {
scheduleSidebarErrors();
});
extractField.appendChild(extractLabel);
extractField.appendChild(extractInput);
body.appendChild(extractField);
const appearanceSection = buildAppearanceSection(
{
theme: site.theme || "inherit",
toolbarPosition: site.toolbarPosition || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
site.alwaysUseDefaultEnvProfile
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
site.emptyToolbarBehavior
)
},
{ stateKey: `site:${card.dataset.id}:appearance` }
);
body.appendChild(appearanceSection);
const disabledInherited = site.disabledInherited || {};
const globalApiConfigs = collectApiConfigs();
const workspace =
allWorkspaces.find((ws) => ws.id === wsSelect.value) || null;
const workspaceDisabled = workspace?.disabledInherited || {};
const apiConfigSection = document.createElement("details");
apiConfigSection.className = "panel sub-panel";
apiConfigSection.dataset.stateKey = `site:${card.dataset.id}:apiConfigs`;
const apiSummary = document.createElement("summary");
apiSummary.className = "panel-summary";
const apiSummaryRow = document.createElement("div");
apiSummaryRow.className = "panel-summary-row";
const apiSummaryLeft = document.createElement("div");
apiSummaryLeft.className = "panel-summary-left";
const apiSummaryRight = document.createElement("div");
apiSummaryRight.className = "panel-summary-right";
const apiSummaryTitle = document.createElement("h3");
apiSummaryTitle.textContent = "API Configurations";
apiSummaryTitle.style.display = "inline";
apiSummaryTitle.style.fontSize = "13px";
apiSummaryTitle.style.fontWeight = "600";
apiSummaryTitle.style.margin = "0";
const apiSummaryLink = document.createElement("button");
apiSummaryLink.type = "button";
apiSummaryLink.className = "panel-meta-link hint-accent";
apiSummaryLink.textContent = "Global";
apiSummaryLink.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
focusGlobalApiConfigSection();
});
apiSummaryLeft.appendChild(apiSummaryTitle);
apiSummaryLeft.appendChild(apiSummaryLink);
apiSummaryRow.appendChild(apiSummaryLeft);
apiSummaryRow.appendChild(apiSummaryRight);
apiSummary.appendChild(apiSummaryRow);
apiConfigSection.appendChild(apiSummary);
const apiBody = document.createElement("div");
apiBody.className = "panel-body";
const workspaceApiEnabled = globalApiConfigs.filter(
(config) =>
isEnabled(config.enabled) &&
!(workspaceDisabled.apiConfigs || []).includes(config.id)
);
const apiList = buildApiConfigToggleList(
workspaceApiEnabled,
disabledInherited.apiConfigs || []
);
apiList.dataset.module = "apiConfigs";
apiBody.appendChild(apiList);
apiConfigSection.appendChild(apiBody);
registerDetail(apiConfigSection, apiConfigSection.open);
body.appendChild(apiConfigSection);
const resolveWorkspaceScope = () => {
const selectedWorkspaceId = wsSelect.value || "global";
const workspaceCard = document.querySelector(
`.workspace-card[data-id="${selectedWorkspaceId}"]`
);
if (workspaceCard) {
return getWorkspaceScopeData(workspaceCard);
}
return {
envs: collectEnvConfigs(),
profiles: collectProfiles(),
tasks: collectTasks(),
shortcuts: collectShortcuts()
};
};
const envSection = buildScopedModuleSection({
title: "Environments",
module: "envs",
parentItems: () => resolveWorkspaceScope().envs,
localItems: site.envConfigs || [],
disabledNames: disabledInherited.envs,
localLabel: "Site-specific",
localContainerClass: "site-envs",
buildCard: buildEnvConfigCard,
stateKey: `site:${card.dataset.id}:envs`,
inheritedFrom: getSiteInheritedSource("envs"),
newItemFactory: (container) => ({
id: newEnvConfigId(),
name: buildUniqueNumberedName(
"New Environment",
collectNames(container, ".env-config-name")
),
apiConfigId: getSiteApiConfigs(card)[0]?.id || "",
systemPrompt: DEFAULT_SYSTEM_PROMPT,
enabled: true
})
});
body.appendChild(envSection.details);
const profileSection = buildScopedModuleSection({
title: "Profiles",
module: "profiles",
parentItems: () => resolveWorkspaceScope().profiles,
localItems: site.profiles || [],
disabledNames: disabledInherited.profiles,
localLabel: "Site-specific",
localContainerClass: "site-profiles",
buildCard: buildProfileCard,
stateKey: `site:${card.dataset.id}:profiles`,
inheritedFrom: getSiteInheritedSource("profiles"),
newItemFactory: (container) => ({
id: newProfileId(),
name: buildUniqueNumberedName(
"New Profile",
collectNames(container, ".profile-name")
),
text: "",
enabled: true
})
});
body.appendChild(profileSection.details);
const taskSection = buildScopedModuleSection({
title: "Tasks",
module: "tasks",
parentItems: () => resolveWorkspaceScope().tasks,
localItems: site.tasks || [],
disabledNames: disabledInherited.tasks,
localLabel: "Site-specific",
localContainerClass: "site-tasks",
buildCard: buildTaskCard,
stateKey: `site:${card.dataset.id}:tasks`,
inheritedFrom: getSiteInheritedSource("tasks"),
cardOptions: () => {
const scope = getSiteScopeData(card);
return { envs: scope.envs, profiles: scope.profiles };
},
newItemFactory: (container) => {
const scope = getSiteScopeData(card);
return {
id: newTaskId(),
name: buildUniqueNumberedName(
"New Task",
collectNames(container, ".task-name")
),
text: "",
defaultEnvId: scope.envs[0]?.id || "",
defaultProfileId: scope.profiles[0]?.id || "",
enabled: true
};
}
});
body.appendChild(taskSection.details);
const shortcutSection = buildScopedModuleSection({
title: "Toolbar Shortcuts",
module: "shortcuts",
parentItems: () => resolveWorkspaceScope().shortcuts,
localItems: site.shortcuts || [],
disabledNames: disabledInherited.shortcuts,
localLabel: "Site-specific",
localContainerClass: "site-shortcuts",
buildCard: buildShortcutCard,
stateKey: `site:${card.dataset.id}:shortcuts`,
inheritedFrom: getSiteInheritedSource("shortcuts"),
cardOptions: () => {
const scope = getSiteScopeData(card);
return { envs: scope.envs, profiles: scope.profiles, tasks: scope.tasks };
},
newItemFactory: (container) => {
const scope = getSiteScopeData(card);
return {
id: newShortcutId(),
name: buildUniqueNumberedName(
"New Shortcut",
collectNames(container, ".shortcut-name")
),
envId: scope.envs[0]?.id || "",
profileId: scope.profiles[0]?.id || "",
taskId: scope.tasks[0]?.id || "",
enabled: true
};
}
});
body.appendChild(shortcutSection.details);
return card;
}
function buildTaskCard(task, container = tasksContainer, options = {}) {
const card = document.createElement("details");
card.className = "task-card";
card.dataset.id = task.id || newTaskId();
card.dataset.stateKey = `task:${card.dataset.id}`;
const taskKey = String(card.dataset.id || "").replace(/[^a-zA-Z0-9_-]/g, "_");
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.className = "config-enabled";
enabledInput.checked = task.enabled !== false;
enabledInput.addEventListener("change", () => {
updateShortcutOptions();
scheduleSidebarErrors();
});
enabledInput.classList.add("hidden");
const nameLabel = document.createElement("label");
nameLabel.textContent = "Name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.id = `task-name-${taskKey}`;
nameInput.value = task.name || "";
nameInput.className = "task-name task-field-input";
nameLabel.htmlFor = nameInput.id;
nameLabel.className = "task-field-label";
const { body, summaryLeft, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled"
);
const enabledToggle = buildEnabledToggleButton(enabledInput);
summaryLeft.prepend(enabledToggle);
summaryLeft.appendChild(enabledInput);
const envLabel = document.createElement("label");
envLabel.textContent = "Default environment";
const envSelect = document.createElement("select");
envSelect.id = `task-env-${taskKey}`;
envSelect.className = "task-env-select task-field-input";
envSelect.dataset.preferred = task.defaultEnvId || "";
envLabel.htmlFor = envSelect.id;
envLabel.className = "task-field-label";
const profileLabel = document.createElement("label");
profileLabel.textContent = "Default profile";
const profileSelect = document.createElement("select");
profileSelect.id = `task-profile-${taskKey}`;
profileSelect.className = "task-profile-select task-field-input";
profileSelect.dataset.preferred = task.defaultProfileId || "";
profileLabel.htmlFor = profileSelect.id;
profileLabel.className = "task-field-label";
const textField = document.createElement("div");
textField.className = "field";
const textLabel = document.createElement("label");
textLabel.textContent = "Task template";
const textArea = document.createElement("textarea");
textArea.rows = 6;
textArea.value = task.text || "";
textArea.className = "task-text";
textField.appendChild(textLabel);
textField.appendChild(textArea);
const envOptions = options.envs
? options.envs
: collectEnvConfigs().filter((env) => isEnabled(env.enabled));
const profileOptions = options.profiles
? options.profiles
: collectProfiles().filter((profile) => isEnabled(profile.enabled));
populateSelect(envSelect, envOptions, "No environments configured");
populateSelect(profileSelect, profileOptions, "No profiles configured");
const actions = document.createElement("div");
actions.className = "task-actions";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "accent add-below";
addBelowBtn.textContent = "Add";
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
moveTopBtn.addEventListener("click", () => {
const first = container.firstElementChild;
if (!first || first === card) return;
animateCardMove(card, () => {
container.insertBefore(card, first);
}, { scrollToCenter: true });
updateTaskControls(container);
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
animateCardMove(card, () => {
container.insertBefore(card, previous);
});
updateTaskControls(container);
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
animateCardMove(card, () => {
container.insertBefore(card, next.nextElementSibling);
});
updateTaskControls(container);
});
addBelowBtn.addEventListener("click", () => {
const name = buildUniqueNumberedName(
"New Task",
collectNames(container, ".task-name")
);
const defaultEnvId =
envSelect.value || envSelect.options[0]?.value || "";
const defaultProfileId =
profileSelect.value || profileSelect.options[0]?.value || "";
const scope = getTaskScopeForContainer(container);
const newCard = buildTaskCard({
id: newTaskId(),
name,
text: "",
defaultEnvId,
defaultProfileId
}, container, scope);
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
centerCardInView(newCard);
updateTaskControls(container);
updateTaskEnvOptions();
updateTaskProfileOptions();
});
const getSourceData = () => ({
id: card.dataset.id,
name: nameInput.value || "Untitled",
text: textArea.value,
defaultEnvId: envSelect.value || "",
defaultProfileId: profileSelect.value || "",
enabled: enabledInput.checked
});
const duplicateControls = buildDuplicateControls("tasks", getSourceData, {
onHere: () => {
const sourceRect = card.getBoundingClientRect();
const scope = getTaskScopeForContainer(container);
const newCard = buildDuplicateCard("tasks", getSourceData(), container, scope);
if (!newCard) return;
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
animateDuplicateFromRect(newCard, sourceRect);
updateTaskControls(container);
updateTaskEnvOptions();
updateTaskProfileOptions();
scheduleSidebarErrors();
centerCardInViewAfterLayout(newCard);
},
sourceCard: card
});
deleteBtn.addEventListener("click", () => {
card.remove();
updateTaskControls(container);
updateShortcutOptions();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
const moveControls = buildMoveControls("tasks", card, container);
actions.appendChild(moveControls);
actions.appendChild(duplicateControls);
actions.appendChild(addBelowBtn);
actions.appendChild(deleteBtn);
const fieldsWrap = document.createElement("div");
fieldsWrap.className = "task-fields";
fieldsWrap.appendChild(nameLabel);
fieldsWrap.appendChild(envLabel);
fieldsWrap.appendChild(profileLabel);
fieldsWrap.appendChild(nameInput);
fieldsWrap.appendChild(envSelect);
fieldsWrap.appendChild(profileSelect);
body.appendChild(fieldsWrap);
body.appendChild(textField);
summaryRight.appendChild(actions);
nameInput.addEventListener("input", () => updateShortcutOptions());
return card;
}
function buildShortcutCard(shortcut, container = shortcutsContainer, options = {}) {
const card = document.createElement("details");
card.className = "shortcut-card";
card.dataset.id = shortcut.id || newShortcutId();
card.dataset.stateKey = `shortcut:${card.dataset.id}`;
const shortcutKey = String(card.dataset.id || "").replace(/[^a-zA-Z0-9_-]/g, "_");
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.className = "config-enabled";
enabledInput.checked = shortcut.enabled !== false;
enabledInput.addEventListener("change", () => {
updateShortcutOptions();
scheduleSidebarErrors();
});
enabledInput.classList.add("hidden");
const nameField = document.createElement("div");
nameField.className = "field";
const nameLabel = document.createElement("label");
nameLabel.textContent = "Name";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.id = `shortcut-name-${shortcutKey}`;
nameInput.value = shortcut.name || "";
nameInput.className = "shortcut-name shortcut-field-input";
nameInput.addEventListener("input", () => {
updateShortcutOptions();
scheduleSidebarErrors();
});
nameLabel.htmlFor = nameInput.id;
nameLabel.className = "shortcut-field-label";
const { body, summaryLeft, summaryRight } = setupCardPanel(
card,
nameInput,
"Untitled"
);
const enabledToggle = buildEnabledToggleButton(enabledInput);
summaryLeft.prepend(enabledToggle);
summaryLeft.appendChild(enabledInput);
nameField.appendChild(nameLabel);
nameField.appendChild(nameInput);
const envLabel = document.createElement("label");
envLabel.textContent = "Environment";
const envSelect = document.createElement("select");
envSelect.id = `shortcut-env-${shortcutKey}`;
envSelect.className = "shortcut-env shortcut-field-input";
envLabel.htmlFor = envSelect.id;
envLabel.className = "shortcut-field-label";
const envs = (options.envs || collectEnvConfigs()).filter((env) =>
isEnabled(env.enabled)
);
for (const env of envs) {
const opt = document.createElement("option");
opt.value = env.id;
opt.textContent = env.name;
envSelect.appendChild(opt);
}
envSelect.value = shortcut.envId || (envs[0]?.id || "");
const profileLabel = document.createElement("label");
profileLabel.textContent = "Profile";
const profileSelect = document.createElement("select");
profileSelect.id = `shortcut-profile-${shortcutKey}`;
profileSelect.className = "shortcut-profile shortcut-field-input";
profileLabel.htmlFor = profileSelect.id;
profileLabel.className = "shortcut-field-label";
const profiles = (options.profiles || collectProfiles()).filter((profile) =>
isEnabled(profile.enabled)
);
for (const p of profiles) {
const opt = document.createElement("option");
opt.value = p.id;
opt.textContent = p.name;
profileSelect.appendChild(opt);
}
profileSelect.value = shortcut.profileId || (profiles[0]?.id || "");
const taskLabel = document.createElement("label");
taskLabel.textContent = "Task";
const taskSelect = document.createElement("select");
taskSelect.id = `shortcut-task-${shortcutKey}`;
taskSelect.className = "shortcut-task shortcut-field-input";
taskLabel.htmlFor = taskSelect.id;
taskLabel.className = "shortcut-field-label";
const tasks = (options.tasks || collectTasks()).filter((task) =>
isEnabled(task.enabled)
);
for (const t of tasks) {
const opt = document.createElement("option");
opt.value = t.id;
opt.textContent = t.name;
taskSelect.appendChild(opt);
}
taskSelect.value = shortcut.taskId || (tasks[0]?.id || "");
const fieldsWrap = document.createElement("div");
fieldsWrap.className = "shortcut-fields";
fieldsWrap.appendChild(nameLabel);
fieldsWrap.appendChild(envLabel);
fieldsWrap.appendChild(profileLabel);
fieldsWrap.appendChild(taskLabel);
fieldsWrap.appendChild(nameInput);
fieldsWrap.appendChild(envSelect);
fieldsWrap.appendChild(profileSelect);
fieldsWrap.appendChild(taskSelect);
const actions = document.createElement("div");
actions.className = "shortcut-actions";
const moveTopBtn = document.createElement("button");
moveTopBtn.type = "button";
moveTopBtn.className = "ghost move-top";
moveTopBtn.textContent = "Top";
const moveUpBtn = document.createElement("button");
moveUpBtn.type = "button";
moveUpBtn.className = "ghost move-up";
moveUpBtn.textContent = "Up";
const moveDownBtn = document.createElement("button");
moveDownBtn.type = "button";
moveDownBtn.className = "ghost move-down";
moveDownBtn.textContent = "Down";
const addBelowBtn = document.createElement("button");
addBelowBtn.type = "button";
addBelowBtn.className = "accent add-below";
addBelowBtn.textContent = "Add";
moveTopBtn.addEventListener("click", () => {
const first = container.firstElementChild;
if (!first || first === card) return;
animateCardMove(card, () => {
container.insertBefore(card, first);
}, { scrollToCenter: true });
updateShortcutControls(container);
updateShortcutOptions();
});
moveUpBtn.addEventListener("click", () => {
const previous = card.previousElementSibling;
if (!previous) return;
animateCardMove(card, () => {
container.insertBefore(card, previous);
});
updateShortcutControls(container);
updateShortcutOptions();
});
moveDownBtn.addEventListener("click", () => {
const next = card.nextElementSibling;
if (!next) return;
animateCardMove(card, () => {
container.insertBefore(card, next.nextElementSibling);
});
updateShortcutControls(container);
updateShortcutOptions();
});
addBelowBtn.addEventListener("click", () => {
const name = buildUniqueNumberedName(
"New Shortcut",
collectNames(container, ".shortcut-name")
);
const newCard = buildShortcutCard({
id: newShortcutId(),
name,
envId: envSelect.value || envs[0]?.id || "",
profileId: profileSelect.value || profiles[0]?.id || "",
taskId: taskSelect.value || tasks[0]?.id || "",
enabled: true
}, container, options);
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
centerCardInView(newCard);
updateShortcutOptions();
updateShortcutControls(container);
});
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "ghost delete";
deleteBtn.textContent = "Delete";
deleteBtn.addEventListener("click", () => {
card.remove();
updateShortcutControls(container);
updateShortcutOptions();
scheduleSidebarErrors();
});
actions.appendChild(moveTopBtn);
actions.appendChild(moveUpBtn);
actions.appendChild(moveDownBtn);
body.appendChild(fieldsWrap);
const moveControls = buildMoveControls("shortcuts", card, container);
const getSourceData = () => ({
id: card.dataset.id,
name: nameInput.value || "Untitled Shortcut",
envId: envSelect.value || "",
profileId: profileSelect.value || "",
taskId: taskSelect.value || "",
enabled: enabledInput.checked
});
const duplicateControls = buildDuplicateControls("shortcuts", getSourceData, {
onHere: () => {
const sourceRect = card.getBoundingClientRect();
const siteCard = container.closest(".site-card");
const workspaceCard = container.closest(".workspace-card");
const scope = siteCard
? getSiteScopeData(siteCard)
: workspaceCard
? getWorkspaceScopeData(workspaceCard)
: {
envs: collectEnvConfigs().filter((env) => isEnabled(env.enabled)),
profiles: collectProfiles().filter((profile) => isEnabled(profile.enabled)),
tasks: collectTasks().filter((task) => isEnabled(task.enabled))
};
const newCard = buildDuplicateCard("shortcuts", getSourceData(), container, scope);
if (!newCard) return;
card.insertAdjacentElement("afterend", newCard);
openDetails(newCard);
animateDuplicateFromRect(newCard, sourceRect);
updateShortcutOptions();
updateShortcutControls(container);
scheduleSidebarErrors();
centerCardInViewAfterLayout(newCard);
},
sourceCard: card
});
actions.appendChild(moveControls);
actions.appendChild(duplicateControls);
actions.appendChild(addBelowBtn);
actions.appendChild(deleteBtn);
summaryRight.appendChild(actions);
return card;
}
function updateShortcutControls(container = shortcutsContainer) {
if (!container) return;
const cards = [...container.querySelectorAll(".shortcut-card")];
cards.forEach((card, index) => {
const moveTopBtn = card.querySelector(".move-top");
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveTopBtn) moveTopBtn.disabled = index === 0;
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
scheduleSidebarErrors();
scheduleTocUpdate();
}
function updateTaskControls(container = tasksContainer) {
const cards = [...container.querySelectorAll(".task-card")];
cards.forEach((card, index) => {
const moveTopBtn = card.querySelector(".move-top");
const moveUpBtn = card.querySelector(".move-up");
const moveDownBtn = card.querySelector(".move-down");
if (moveTopBtn) moveTopBtn.disabled = index === 0;
if (moveUpBtn) moveUpBtn.disabled = index === 0;
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
});
scheduleSidebarErrors();
scheduleTocUpdate();
}
function collectTasks(container = tasksContainer) {
if (!container) return [];
const cards = [...container.querySelectorAll(".task-card")];
return cards.map((card) => {
const nameInput = card.querySelector(".task-name");
const textArea = card.querySelector(".task-text");
const envSelect = card.querySelector(".task-env-select");
const profileSelect = card.querySelector(".task-profile-select");
const enabledInput = card.querySelector(".config-enabled");
return {
id: card.dataset.id || newTaskId(),
name: (nameInput?.value || "Untitled Task").trim(),
text: (textArea?.value || "").trim(),
defaultEnvId: envSelect?.value || "",
defaultProfileId: profileSelect?.value || "",
enabled: enabledInput ? enabledInput.checked : true
};
});
}
function updateSidebarErrors() {
if (!sidebarErrorsEl) return;
const errors = [];
const tasks = collectTasks();
const envs = collectEnvConfigs();
const profiles = collectProfiles();
const apiConfigs = collectApiConfigs();
const apiKeys = collectApiKeys();
const enabledTasks = tasks.filter((task) => isEnabled(task.enabled));
const enabledEnvs = envs.filter((env) => isEnabled(env.enabled));
const enabledProfiles = profiles.filter((profile) => isEnabled(profile.enabled));
const enabledApiConfigs = apiConfigs.filter((config) => isEnabled(config.enabled));
const enabledApiKeys = apiKeys.filter((key) => isEnabled(key.enabled));
const checkNameInputs = (container, selector, label) => {
if (!container) return;
const inputs = [...container.querySelectorAll(selector)];
if (!inputs.length) return;
const seen = new Map();
let hasEmpty = false;
for (const input of inputs) {
const card = input.closest(
".task-card, .env-config-card, .profile-card, .api-config-card, .api-key-card, .shortcut-card"
);
const enabledToggle = card?.querySelector(".config-enabled");
if (enabledToggle && !enabledToggle.checked) {
continue;
}
const name = (input.value || "").trim();
if (!name) {
hasEmpty = true;
continue;
}
const lower = name.toLowerCase();
seen.set(lower, (seen.get(lower) || 0) + 1);
}
if (hasEmpty) {
errors.push(`${label} has empty names.`);
}
for (const [name, count] of seen.entries()) {
if (count > 1) {
errors.push(`${label} has duplicate name: ${name}.`);
}
}
};
checkNameInputs(tasksContainer, ".task-name", "Tasks");
checkNameInputs(envConfigsContainer, ".env-config-name", "Environments");
checkNameInputs(profilesContainer, ".profile-name", "Profiles");
checkNameInputs(shortcutsContainer, ".shortcut-name", "Toolbar shortcuts");
checkNameInputs(apiConfigsContainer, ".api-config-name", "API configs");
checkNameInputs(apiKeysContainer, ".api-key-name", "API keys");
checkNameInputs(workspacesContainer, ".workspace-name", "Workspaces");
const workspaceCards = [
...workspacesContainer.querySelectorAll(".workspace-card")
];
workspaceCards.forEach((card) => {
const name = card.querySelector(".workspace-name")?.value || "Workspace";
checkNameInputs(
card.querySelector(".workspace-envs"),
".env-config-name",
`${name} environments`
);
checkNameInputs(
card.querySelector(".workspace-profiles"),
".profile-name",
`${name} profiles`
);
checkNameInputs(
card.querySelector(".workspace-tasks"),
".task-name",
`${name} tasks`
);
checkNameInputs(
card.querySelector(".workspace-shortcuts"),
".shortcut-name",
`${name} shortcuts`
);
});
const siteCards = [...sitesContainer.querySelectorAll(".site-card")];
siteCards.forEach((card) => {
const label =
card.querySelector(".site-name")?.value ||
card.querySelector(".site-pattern")?.value ||
"Site";
checkNameInputs(
card.querySelector(".site-envs"),
".env-config-name",
`${label} environments`
);
checkNameInputs(
card.querySelector(".site-profiles"),
".profile-name",
`${label} profiles`
);
checkNameInputs(
card.querySelector(".site-tasks"),
".task-name",
`${label} tasks`
);
checkNameInputs(
card.querySelector(".site-shortcuts"),
".shortcut-name",
`${label} shortcuts`
);
const extractInput = card.querySelector(".site-extract-selector");
const { error } = parseExtractionTargetInput(extractInput?.value || "");
if (error) {
errors.push(`${label} site text selector: ${error}`);
}
});
checkNameInputs(sitesContainer, ".site-name", "Sites");
if (!enabledTasks.length) errors.push("No tasks enabled.");
if (!enabledEnvs.length) errors.push("No environments enabled.");
if (!enabledProfiles.length) errors.push("No profiles enabled.");
if (!enabledApiConfigs.length) errors.push("No API configs enabled.");
if (!enabledApiKeys.length) errors.push("No API keys enabled.");
const validateTaskDefaults = (label, taskList, envList, profileList) => {
const enabledEnvIds = new Set(
envList.filter((env) => isEnabled(env.enabled)).map((env) => env.id)
);
const enabledProfileIds = new Set(
profileList
.filter((profile) => isEnabled(profile.enabled))
.map((profile) => profile.id)
);
taskList.filter((task) => isEnabled(task.enabled)).forEach((task) => {
const taskName = task.name || "Untitled Task";
if (enabledEnvIds.size && !task.defaultEnvId) {
errors.push(`${label} task "${taskName}" is missing a default environment.`);
} else if (
task.defaultEnvId &&
enabledEnvIds.size &&
!enabledEnvIds.has(task.defaultEnvId)
) {
errors.push(
`${label} task "${taskName}" default environment is disabled or missing.`
);
}
if (enabledProfileIds.size && !task.defaultProfileId) {
errors.push(`${label} task "${taskName}" is missing a default profile.`);
} else if (
task.defaultProfileId &&
enabledProfileIds.size &&
!enabledProfileIds.has(task.defaultProfileId)
) {
errors.push(
`${label} task "${taskName}" default profile is disabled or missing.`
);
}
});
};
if (enabledTasks.length) {
const defaultTask = enabledTasks[0];
if (!defaultTask.text) errors.push("Default task prompt is empty.");
const defaultEnv =
enabledEnvs.find((env) => env.id === defaultTask.defaultEnvId) ||
enabledEnvs[0];
if (!defaultEnv) {
errors.push("Default task environment is missing.");
}
const defaultProfile =
enabledProfiles.find((profile) => profile.id === defaultTask.defaultProfileId) ||
enabledProfiles[0];
if (!defaultProfile) {
errors.push("Default task profile is missing.");
} else if (!defaultProfile.text) {
errors.push("Default profile text is empty.");
}
const defaultApiConfig = defaultEnv
? enabledApiConfigs.find((config) => config.id === defaultEnv.apiConfigId)
: null;
if (!defaultApiConfig) {
errors.push("Default environment is missing an API config.");
} else if (defaultApiConfig.advanced) {
if (!isValidTemplateJson(defaultApiConfig.requestTemplate || "")) {
errors.push("Default API config request template is invalid JSON.");
}
} else {
if (!defaultApiConfig.apiBaseUrl) {
errors.push("Default API config is missing a base URL.");
}
if (!defaultApiConfig.model) {
errors.push("Default API config is missing a model name.");
}
}
if (defaultApiConfig && !defaultApiConfig.advanced) {
const key = enabledApiKeys.find(
(entry) => entry.id === defaultApiConfig?.apiKeyId
);
if (!key || !key.key) {
errors.push("Default API config is missing an API key.");
}
}
}
validateTaskDefaults("Global", tasks, envs, profiles);
workspaceCards.forEach((card) => {
const name =
card.querySelector(".workspace-name")?.value || "Untitled Workspace";
const scope = getWorkspaceScopeData(card);
const scopedTasks = collectTasks(card.querySelector(".workspace-tasks"));
validateTaskDefaults(`Workspace "${name}"`, scopedTasks, scope.envs, scope.profiles);
});
siteCards.forEach((card) => {
const name =
card.querySelector(".site-name")?.value ||
card.querySelector(".site-pattern")?.value ||
"Untitled Site";
const scope = getSiteScopeData(card);
const scopedTasks = collectTasks(card.querySelector(".site-tasks"));
validateTaskDefaults(`Site "${name}"`, scopedTasks, scope.envs, scope.profiles);
});
const sites = collectSites();
const patterns = siteCards
.map((card) => (card.querySelector(".site-pattern")?.value || "").trim())
.filter(Boolean);
for (let i = 0; i < patterns.length; i += 1) {
for (let j = 0; j < patterns.length; j += 1) {
if (i === j) continue;
if (patterns[j].includes(patterns[i])) {
errors.push(
`Site URL pattern "${patterns[i]}" is a substring of "${patterns[j]}".`
);
break;
}
}
}
workspaceCards.forEach((card) => {
const list = card.querySelector(".workspace-sites-list");
if (!list) return;
renderWorkspaceSitesList(list, card.dataset.id, sites);
});
if (!errors.length) {
sidebarErrorsEl.classList.add("hidden");
sidebarErrorsEl.textContent = "";
renderGlobalSitesList(sites);
return;
}
sidebarErrorsEl.textContent = errors.map((error) => `- ${error}`).join("\n");
sidebarErrorsEl.classList.remove("hidden");
renderGlobalSitesList(sites);
}
async function loadSettings() {
let {
apiKey = "",
apiKeys = [],
activeApiKeyId = "",
apiConfigs = [],
activeApiConfigId = "",
envConfigs = [],
activeEnvConfigId = "",
profiles = [],
apiBaseUrl = "",
model = "",
systemPrompt = "",
resume = "",
tasks = [],
shortcuts = [],
presets: legacyPresets = [],
theme = "system",
workspaces = [],
sites = [],
toolbarPosition = "bottom-right",
toolbarAutoHide: storedToolbarAutoHide = true,
alwaysShowOutput: storedAlwaysShowOutput = false,
alwaysUseDefaultEnvProfile: storedAlwaysUseDefaultEnvProfile = false,
emptyToolbarBehavior: storedEmptyToolbarBehavior = "open",
sidebarWidth
} = await getStorage([
"apiKey",
"apiKeys",
"activeApiKeyId",
"apiConfigs",
"activeApiConfigId",
"envConfigs",
"activeEnvConfigId",
"profiles",
"apiBaseUrl",
"model",
"systemPrompt",
"resume",
"tasks",
"shortcuts",
"presets",
"theme",
"workspaces",
"sites",
"toolbarPosition",
"toolbarAutoHide",
"emptyToolbarBehavior",
"alwaysUseDefaultEnvProfile",
SIDEBAR_WIDTH_KEY
]);
themeSelect.value = theme;
applyTheme(theme);
if (toolbarPositionSelect) {
toolbarPositionSelect.value = toolbarPosition;
}
if (toolbarAutoHide) {
toolbarAutoHide.checked = Boolean(storedToolbarAutoHide);
}
if (emptyToolbarBehaviorSelect) {
emptyToolbarBehaviorSelect.value = normalizeEmptyToolbarBehavior(
storedEmptyToolbarBehavior,
false
);
}
if (alwaysShowOutput) {
alwaysShowOutput.checked = Boolean(storedAlwaysShowOutput);
}
if (alwaysUseDefaultEnvProfileSelect) {
const normalizedDefault = normalizeAppearanceToggle(
storedAlwaysUseDefaultEnvProfile
);
alwaysUseDefaultEnvProfileSelect.value =
normalizedDefault === "enabled" ? "enabled" : "disabled";
}
if (Number.isFinite(sidebarWidth)) {
applySidebarWidth(sidebarWidth);
}
if (!shortcuts.length && Array.isArray(legacyPresets) && legacyPresets.length) {
shortcuts = legacyPresets;
await chrome.storage.local.set({ shortcuts });
await chrome.storage.local.remove("presets");
}
if (Array.isArray(workspaces)) {
let needsWorkspaceUpdate = false;
const normalizedWorkspaces = workspaces.map((workspace) => {
if (!workspace || typeof workspace !== "object") return workspace;
const { presets, shortcuts: wsShortcuts, ...rest } = workspace;
const resolvedShortcuts =
Array.isArray(wsShortcuts) && wsShortcuts.length
? wsShortcuts
: Array.isArray(presets)
? presets
: [];
if (presets !== undefined || wsShortcuts === undefined) {
needsWorkspaceUpdate = true;
}
return { ...rest, shortcuts: resolvedShortcuts };
});
if (needsWorkspaceUpdate) {
await chrome.storage.local.set({ workspaces: normalizedWorkspaces });
}
workspaces = normalizedWorkspaces.map((workspace) => {
if (!workspace || typeof workspace !== "object") return workspace;
return {
...workspace,
theme: workspace.theme || "inherit",
toolbarPosition: workspace.toolbarPosition || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
workspace.alwaysUseDefaultEnvProfile
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
workspace.emptyToolbarBehavior
),
envConfigs: normalizeConfigList(workspace.envConfigs),
profiles: normalizeConfigList(workspace.profiles),
tasks: normalizeConfigList(workspace.tasks),
shortcuts: normalizeConfigList(workspace.shortcuts),
disabledInherited: normalizeDisabledInherited(workspace.disabledInherited)
};
});
}
if (Array.isArray(sites)) {
let needsSiteUpdate = false;
sites = sites.map((site) => {
if (!site || typeof site !== "object") return site;
const normalizedTarget = normalizeStoredExtractionTarget(site);
if (normalizedTarget.changed) {
needsSiteUpdate = true;
}
return {
...site,
name: site.name || site.urlPattern || "",
workspaceId: site.workspaceId || "global",
extractTarget: normalizedTarget.target,
theme: site.theme || "inherit",
toolbarPosition: site.toolbarPosition || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
site.alwaysUseDefaultEnvProfile
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
site.emptyToolbarBehavior
),
envConfigs: normalizeConfigList(site.envConfigs),
profiles: normalizeConfigList(site.profiles),
tasks: normalizeConfigList(site.tasks),
shortcuts: normalizeConfigList(site.shortcuts),
disabledInherited: normalizeDisabledInherited(site.disabledInherited)
};
});
if (needsSiteUpdate) {
await chrome.storage.local.set({ sites });
}
}
initialSiteIds = new Set((sites || []).map((site) => site?.id).filter(Boolean));
// Load basic resources first so they are available for shortcuts/workspaces
envConfigsContainer.innerHTML = "";
// ... (existing logic handles this later)
// Wait, I need to make sure collectEnvConfigs etc work.
// loadSettings currently renders cards later in the function.
// I need to ensure render order.
// Actually, loadSettings renders cards in order. I should just add shortcuts rendering at the end.
// I'll render shortcuts after tasks are rendered.
let resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
let resolvedActiveId = activeApiKeyId;
if (!resolvedKeys.length && apiKey) {
const migrated = {
id: newApiKeyId(),
name: "Default",
key: apiKey,
enabled: true
};
resolvedKeys = [migrated];
resolvedActiveId = migrated.id;
await chrome.storage.local.set({
apiKeys: resolvedKeys,
activeApiKeyId: resolvedActiveId
});
} else if (resolvedKeys.length) {
const normalized = resolvedKeys.map((entry) => ({
...entry,
enabled: entry.enabled !== false
}));
if (normalized.some((entry, index) => entry.enabled !== resolvedKeys[index]?.enabled)) {
resolvedKeys = normalized;
await chrome.storage.local.set({ apiKeys: resolvedKeys });
} else {
resolvedKeys = normalized;
}
const hasActive = resolvedKeys.some((entry) => entry.id === resolvedActiveId);
if (!hasActive) {
resolvedActiveId = resolvedKeys[0].id;
await chrome.storage.local.set({ activeApiKeyId: resolvedActiveId });
}
}
apiKeysContainer.innerHTML = "";
if (!resolvedKeys.length) {
apiKeysContainer.appendChild(
buildApiKeyCard({ id: newApiKeyId(), name: "", key: "" })
);
} else {
for (const entry of resolvedKeys) {
apiKeysContainer.appendChild(buildApiKeyCard(entry));
}
}
updateApiKeyControls();
let resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : [];
let resolvedActiveConfigId = activeApiConfigId;
if (!resolvedConfigs.length) {
const migrated = {
id: newApiConfigId(),
name: "Default",
apiBaseUrl: apiBaseUrl || OPENAI_DEFAULTS.apiBaseUrl,
model: model || DEFAULT_MODEL,
apiKeyId: resolvedActiveId || resolvedKeys[0]?.id || "",
apiUrl: "",
requestTemplate: "",
advanced: false,
enabled: true
};
resolvedConfigs = [migrated];
resolvedActiveConfigId = migrated.id;
await chrome.storage.local.set({
apiConfigs: resolvedConfigs,
activeApiConfigId: resolvedActiveConfigId
});
} else {
const fallbackKeyId = resolvedActiveId || resolvedKeys[0]?.id || "";
const withKeys = resolvedConfigs.map((config) => ({
...config,
apiKeyId: config.apiKeyId || fallbackKeyId,
apiUrl: config.apiUrl || "",
requestTemplate: config.requestTemplate || "",
advanced: Boolean(config.advanced),
enabled: config.enabled !== false
}));
if (
withKeys.some(
(config, index) =>
config.apiKeyId !== resolvedConfigs[index].apiKeyId ||
config.enabled !== resolvedConfigs[index].enabled
)
) {
resolvedConfigs = withKeys;
await chrome.storage.local.set({ apiConfigs: resolvedConfigs });
}
const hasActive = resolvedConfigs.some(
(config) => config.id === resolvedActiveConfigId
);
if (!hasActive) {
resolvedActiveConfigId = resolvedConfigs[0].id;
await chrome.storage.local.set({ activeApiConfigId: resolvedActiveConfigId });
}
}
apiConfigsContainer.innerHTML = "";
for (const config of resolvedConfigs) {
apiConfigsContainer.appendChild(buildApiConfigCard(config));
}
updateApiConfigKeyOptions();
updateApiConfigControls();
let resolvedEnvConfigs = Array.isArray(envConfigs) ? envConfigs : [];
const fallbackApiConfigId =
resolvedActiveConfigId || resolvedConfigs[0]?.id || "";
if (!resolvedEnvConfigs.length) {
const migrated = {
id: newEnvConfigId(),
name: "Default",
apiConfigId: fallbackApiConfigId,
systemPrompt: systemPrompt || DEFAULT_SYSTEM_PROMPT,
enabled: true
};
resolvedEnvConfigs = [migrated];
await chrome.storage.local.set({
envConfigs: resolvedEnvConfigs,
activeEnvConfigId: migrated.id
});
} else {
const withDefaults = resolvedEnvConfigs.map((config) => ({
...config,
apiConfigId: config.apiConfigId || fallbackApiConfigId,
systemPrompt: config.systemPrompt ?? "",
enabled: config.enabled !== false
}));
const needsUpdate = withDefaults.some((config, index) => {
const original = resolvedEnvConfigs[index];
return (
config.apiConfigId !== original.apiConfigId ||
(config.systemPrompt || "") !== (original.systemPrompt || "") ||
config.enabled !== original.enabled
);
});
if (needsUpdate) {
resolvedEnvConfigs = withDefaults;
await chrome.storage.local.set({ envConfigs: resolvedEnvConfigs });
}
const hasActive = resolvedEnvConfigs.some(
(config) => config.id === activeEnvConfigId
);
if (!hasActive && resolvedEnvConfigs.length) {
await chrome.storage.local.set({
activeEnvConfigId: resolvedEnvConfigs[0].id
});
}
}
envConfigsContainer.innerHTML = "";
for (const config of resolvedEnvConfigs) {
envConfigsContainer.appendChild(buildEnvConfigCard(config));
}
updateEnvApiOptions();
updateEnvControls();
let resolvedProfiles = Array.isArray(profiles) ? profiles : [];
if (!resolvedProfiles.length) {
const migrated = {
id: newProfileId(),
name: "Default",
text: resume || "",
type: "Resume",
enabled: true
};
resolvedProfiles = [migrated];
await chrome.storage.local.set({ profiles: resolvedProfiles });
} else {
const normalized = resolvedProfiles.map((profile) => ({
...profile,
text: profile.text ?? "",
type: profile.type === "Profile" ? "Profile" : "Resume",
enabled: profile.enabled !== false
}));
const needsUpdate = normalized.some(
(profile, index) =>
(profile.text || "") !== (resolvedProfiles[index]?.text || "") ||
(profile.type || "Resume") !== (resolvedProfiles[index]?.type || "Resume") ||
profile.enabled !== resolvedProfiles[index]?.enabled
);
if (needsUpdate) {
resolvedProfiles = normalized;
await chrome.storage.local.set({ profiles: resolvedProfiles });
}
}
profilesContainer.innerHTML = "";
for (const profile of resolvedProfiles) {
profilesContainer.appendChild(buildProfileCard(profile));
}
updateProfileControls();
tasksContainer.innerHTML = "";
const defaultEnvId = resolvedEnvConfigs[0]?.id || "";
const defaultProfileId = resolvedProfiles[0]?.id || "";
const normalizedTasks = Array.isArray(tasks)
? tasks.map((task) => ({
...task,
defaultEnvId: task.defaultEnvId || defaultEnvId,
defaultProfileId: task.defaultProfileId || defaultProfileId,
enabled: task.enabled !== false
}))
: [];
if (
normalizedTasks.length &&
normalizedTasks.some(
(task, index) =>
task.defaultEnvId !== tasks[index]?.defaultEnvId ||
task.defaultProfileId !== tasks[index]?.defaultProfileId ||
task.enabled !== tasks[index]?.enabled
)
) {
await chrome.storage.local.set({ tasks: normalizedTasks });
}
if (!normalizedTasks.length) {
tasksContainer.appendChild(
buildTaskCard({
id: newTaskId(),
name: "",
text: "",
defaultEnvId,
defaultProfileId
})
);
updateTaskControls();
updateTaskEnvOptions();
updateTaskProfileOptions();
return;
}
for (const task of normalizedTasks) {
tasksContainer.appendChild(buildTaskCard(task));
}
updateTaskControls();
updateTaskEnvOptions();
updateTaskProfileOptions();
const normalizedShortcuts = Array.isArray(shortcuts)
? shortcuts.map((shortcut) => ({
...shortcut,
enabled: shortcut.enabled !== false
}))
: [];
shortcuts = normalizedShortcuts;
if (
normalizedShortcuts.length &&
normalizedShortcuts.some(
(shortcut, index) => shortcut.enabled !== shortcuts[index]?.enabled
)
) {
await chrome.storage.local.set({ shortcuts: normalizedShortcuts });
}
shortcutsContainer.innerHTML = "";
for (const shortcut of normalizedShortcuts) {
shortcutsContainer.appendChild(buildShortcutCard(shortcut));
}
updateShortcutControls();
workspacesContainer.innerHTML = "";
for (const ws of workspaces) {
workspacesContainer.appendChild(buildWorkspaceCard(ws, workspaces, sites));
}
sitesContainer.innerHTML = "";
for (const site of sites) {
sitesContainer.appendChild(buildSiteCard(site, workspaces));
}
updateEnvApiOptions();
refreshWorkspaceInheritedLists();
refreshSiteInheritedLists();
updateSidebarErrors();
updateToc(workspaces, sites);
renderGlobalSitesList(sites);
updateAppearanceInheritanceIndicators();
}
async function saveSettings() {
try {
const tasks = collectTasks();
const shortcuts = collectShortcuts();
const apiKeys = collectApiKeys();
const apiConfigs = collectApiConfigs();
const envConfigs = collectEnvConfigs();
const profiles = collectProfiles();
const workspaces = collectWorkspaces();
const sites = collectSites();
const previous = await getStorage([
"apiConfigs",
"envConfigs",
"profiles",
"tasks",
"shortcuts",
"workspaces",
"sites"
]);
const previousWorkspaces = Array.isArray(previous.workspaces)
? previous.workspaces
: [];
const globalRenameMaps = {
envs: buildRenameMap(previous.envConfigs, envConfigs),
profiles: buildRenameMap(previous.profiles, profiles),
tasks: buildRenameMap(previous.tasks, tasks),
shortcuts: buildRenameMap(previous.shortcuts, shortcuts)
};
const previousWorkspaceById = new Map(
previousWorkspaces.map((workspace) => [workspace.id, workspace])
);
const workspaceRenameMaps = new Map(
workspaces.map((workspace) => {
const previousWorkspace = previousWorkspaceById.get(workspace.id);
return [
workspace.id,
{
envs: buildRenameMap(previousWorkspace?.envConfigs, workspace.envConfigs),
profiles: buildRenameMap(previousWorkspace?.profiles, workspace.profiles),
tasks: buildRenameMap(previousWorkspace?.tasks, workspace.tasks),
shortcuts: buildRenameMap(
previousWorkspace?.shortcuts,
workspace.shortcuts
)
}
];
})
);
const updatedWorkspaces = workspaces.map((workspace) => {
const disabled = workspace.disabledInherited || {};
return {
...workspace,
disabledInherited: {
...disabled,
envs: applyRenameMaps(disabled.envs, [globalRenameMaps.envs]),
profiles: applyRenameMaps(disabled.profiles, [globalRenameMaps.profiles]),
tasks: applyRenameMaps(disabled.tasks, [globalRenameMaps.tasks]),
shortcuts: applyRenameMaps(disabled.shortcuts, [globalRenameMaps.shortcuts]),
apiConfigs: filterDisabledIds(disabled.apiConfigs, apiConfigs)
}
};
});
const updatedSites = sites.map((site) => {
const workspaceId = site.workspaceId || "global";
const maps = workspaceRenameMaps.get(workspaceId) || {};
const disabled = site.disabledInherited || {};
return {
...site,
disabledInherited: {
...disabled,
envs: applyRenameMaps(disabled.envs, [
globalRenameMaps.envs,
maps.envs
]),
profiles: applyRenameMaps(disabled.profiles, [
globalRenameMaps.profiles,
maps.profiles
]),
tasks: applyRenameMaps(disabled.tasks, [
globalRenameMaps.tasks,
maps.tasks
]),
shortcuts: applyRenameMaps(disabled.shortcuts, [
globalRenameMaps.shortcuts,
maps.shortcuts
]),
apiConfigs: filterDisabledIds(disabled.apiConfigs, apiConfigs)
}
};
});
const storedSites = Array.isArray(previous.sites) ? previous.sites : [];
const mergedSites = [...updatedSites];
const mergedIds = new Set(updatedSites.map((site) => site.id));
storedSites.forEach((site) => {
if (!site?.id) return;
if (mergedIds.has(site.id)) return;
if (initialSiteIds.has(site.id)) return;
mergedIds.add(site.id);
mergedSites.push(site);
});
const activeEnvConfigId = envConfigs[0]?.id || "";
const activeEnv = envConfigs[0];
const activeApiConfigId =
activeEnv?.apiConfigId || apiConfigs[0]?.id || "";
const activeConfig = apiConfigs.find(
(entry) => entry.id === activeApiConfigId
);
const activeApiKeyId =
activeConfig?.apiKeyId ||
apiKeys[0]?.id ||
"";
await chrome.storage.local.set({
apiKeys,
activeApiKeyId,
apiConfigs,
activeApiConfigId,
envConfigs,
activeEnvConfigId,
systemPrompt: activeEnv?.systemPrompt || "",
profiles,
tasks,
shortcuts,
theme: themeSelect.value,
toolbarPosition: toolbarPositionSelect
? toolbarPositionSelect.value
: "bottom-right",
emptyToolbarBehavior: emptyToolbarBehaviorSelect
? emptyToolbarBehaviorSelect.value
: "open",
toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true,
alwaysShowOutput: alwaysShowOutput ? alwaysShowOutput.checked : false,
alwaysUseDefaultEnvProfile: alwaysUseDefaultEnvProfileSelect
? alwaysUseDefaultEnvProfileSelect.value === "enabled"
: false,
workspaces: updatedWorkspaces,
sites: mergedSites
});
await chrome.storage.local.remove("presets");
captureSavedSnapshot();
setStatus("Saved.");
} catch (error) {
console.error("Save failed:", error);
setStatus("Save failed. Check console.");
}
}
if (saveBtnSidebar) {
saveBtnSidebar.addEventListener("click", () => void saveSettings());
}
addTaskBtn.addEventListener("click", () => {
openDetails(addTaskBtn.closest("details"));
const name = buildUniqueNumberedName(
"New Task",
collectNames(tasksContainer, ".task-name")
);
const newCard = buildTaskCard({
id: newTaskId(),
name,
text: "",
defaultEnvId: getTopEnvId(),
defaultProfileId: getTopProfileId()
}, tasksContainer);
const first = tasksContainer.firstElementChild;
if (first) {
tasksContainer.insertBefore(newCard, first);
} else {
tasksContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
updateTaskControls(tasksContainer);
updateTaskEnvOptions();
updateTaskProfileOptions();
});
addApiKeyBtn.addEventListener("click", () => {
openDetails(addApiKeyBtn.closest("details"));
const name = buildUniqueDefaultName(
collectNames(apiKeysContainer, ".api-key-name")
);
const newCard = buildApiKeyCard({ id: newApiKeyId(), name, key: "" });
const first = apiKeysContainer.firstElementChild;
if (first) {
apiKeysContainer.insertBefore(newCard, first);
} else {
apiKeysContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
updateApiConfigKeyOptions();
updateApiKeyControls();
});
addApiConfigBtn.addEventListener("click", () => {
openDetails(addApiConfigBtn.closest("details"));
const name = buildUniqueNumberedName(
"New API",
collectNames(apiConfigsContainer, ".api-config-name")
);
const newCard = buildApiConfigCard({
id: newApiConfigId(),
name,
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
model: DEFAULT_MODEL,
apiUrl: "",
requestTemplate: "",
advanced: false
});
const first = apiConfigsContainer.firstElementChild;
if (first) {
apiConfigsContainer.insertBefore(newCard, first);
} else {
apiConfigsContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
updateApiConfigKeyOptions();
updateEnvApiOptions();
updateApiConfigControls();
});
addEnvConfigBtn.addEventListener("click", () => {
openDetails(addEnvConfigBtn.closest("details"));
const name = buildUniqueNumberedName(
"New Environment",
collectNames(envConfigsContainer, ".env-config-name")
);
const fallbackApiConfigId =
getApiConfigsForEnvContainer(envConfigsContainer)[0]?.id || "";
const newCard = buildEnvConfigCard({
id: newEnvConfigId(),
name,
apiConfigId: fallbackApiConfigId,
systemPrompt: DEFAULT_SYSTEM_PROMPT
});
const first = envConfigsContainer.firstElementChild;
if (first) {
envConfigsContainer.insertBefore(newCard, first);
} else {
envConfigsContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
updateEnvApiOptions();
updateEnvControls();
updateTaskEnvOptions();
});
addProfileBtn.addEventListener("click", () => {
openDetails(addProfileBtn.closest("details"));
const name = buildUniqueNumberedName(
"New Profile",
collectNames(profilesContainer, ".profile-name")
);
const newCard = buildProfileCard({
id: newProfileId(),
name,
text: ""
}, profilesContainer);
const first = profilesContainer.firstElementChild;
if (first) {
profilesContainer.insertBefore(newCard, first);
} else {
profilesContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
updateProfileControls(profilesContainer);
updateTaskProfileOptions();
});
addWorkspaceBtn.addEventListener("click", () => {
openDetails(addWorkspaceBtn.closest("details"));
const newCard = buildWorkspaceCard({
id: newWorkspaceId(),
name: "New Workspace",
theme: "inherit",
toolbarPosition: "inherit",
alwaysUseDefaultEnvProfile: "inherit",
emptyToolbarBehavior: "inherit",
envConfigs: [],
profiles: [],
tasks: [],
shortcuts: [],
disabledInherited: normalizeDisabledInherited()
}, collectWorkspaces(), collectSites());
const first = workspacesContainer.firstElementChild;
if (first) {
workspacesContainer.insertBefore(newCard, first);
} else {
workspacesContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
refreshWorkspaceInheritedLists();
scheduleSidebarErrors();
updateAppearanceInheritanceIndicators();
updateToc(collectWorkspaces(), collectSites());
});
addSiteBtn.addEventListener("click", () => {
openDetails(addSiteBtn.closest("details"));
const newCard = buildSiteCard({
id: newSiteId(),
name: "",
urlPattern: "",
workspaceId: "global",
theme: "inherit",
toolbarPosition: "inherit",
alwaysUseDefaultEnvProfile: "inherit",
emptyToolbarBehavior: "inherit",
envConfigs: [],
profiles: [],
tasks: [],
shortcuts: [],
disabledInherited: normalizeDisabledInherited()
}, collectWorkspaces());
const first = sitesContainer.firstElementChild;
if (first) {
sitesContainer.insertBefore(newCard, first);
} else {
sitesContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
refreshSiteInheritedLists();
scheduleSidebarErrors();
updateAppearanceInheritanceIndicators();
updateToc(collectWorkspaces(), collectSites());
});
addShortcutBtn.addEventListener("click", () => {
openDetails(addShortcutBtn.closest("details"));
const name = buildUniqueNumberedName(
"New Shortcut",
collectNames(shortcutsContainer, ".shortcut-name")
);
const newCard = buildShortcutCard({
id: newShortcutId(),
name,
envId: "",
profileId: "",
taskId: ""
});
const first = shortcutsContainer.firstElementChild;
if (first) {
shortcutsContainer.insertBefore(newCard, first);
} else {
shortcutsContainer.appendChild(newCard);
}
openDetails(newCard);
centerCardInView(newCard);
updateShortcutOptions();
updateShortcutControls();
scheduleSidebarErrors();
});
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
initSidebarResize();
document.addEventListener("click", (event) => {
const summary = event.target.closest("summary.panel-summary");
if (!summary) return;
if (event.target.closest("button")) {
event.preventDefault();
event.stopPropagation();
}
});
async function initSettings() {
suppressDirtyTracking = true;
await loadSettingsViewState();
await loadSettings();
initToc();
registerAllDetails();
restoreScrollPosition();
initDirtyObserver();
if (settingsLayout) {
settingsLayout.addEventListener("input", handleTocNameInput);
}
captureSavedSnapshot();
suppressDirtyTracking = false;
window.addEventListener("scroll", handleSettingsScroll, { passive: true });
}
void initSettings();
function openDetailsChain(target) {
let node = target;
while (node) {
if (node.tagName === "DETAILS") {
node.open = true;
}
node = node.parentElement?.closest("details");
}
}
function renderTocCardList(listEl, cards, nameSelector, fallbackLabel, onClick) {
if (!listEl) return;
listEl.innerHTML = "";
const items = Array.from(cards || []);
items.forEach((card, index) => {
const name =
card.querySelector(nameSelector)?.value?.trim() ||
`${fallbackLabel} ${index + 1}`;
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "#";
a.textContent = name;
const cardClass =
[...(card?.classList || [])].find((cls) => cls.endsWith("-card")) ||
card?.classList?.[0] ||
"";
if (cardClass && card?.dataset?.id) {
a.dataset.tocTargetSelector = `.${cardClass}[data-id="${card.dataset.id}"]`;
}
a.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (typeof onClick === "function") {
onClick(card);
}
});
li.appendChild(a);
listEl.appendChild(li);
});
}
function updateToc(workspaces, sites) {
const wsList = document.getElementById("toc-workspaces-list");
if (!wsList) return;
const existingGroups = wsList.querySelectorAll("details.toc-group");
existingGroups.forEach((group) => {
const key = getDetailStateKey(group);
if (!key) return;
settingsViewState.open[key] = group.open;
});
scheduleSettingsViewStateSave();
wsList.innerHTML = "";
for (const ws of workspaces) {
const li = document.createElement("li");
const details = document.createElement("details");
details.className = "toc-group toc-workspace";
details.dataset.stateKey = `toc:workspace:${ws.id}`;
const summary = document.createElement("summary");
const a = document.createElement("a");
a.href = "#";
a.textContent = ws.name || "Untitled";
a.dataset.tocTargetSelector = `.workspace-card[data-id="${ws.id}"]`;
summary.appendChild(a);
details.appendChild(summary);
const subUl = document.createElement("ul");
subUl.className = "toc-sub";
const sectionConfigs = [
{ label: "Appearance" },
{ label: "API Configurations" },
{
label: "Environments",
module: "envs",
containerSelector: ".workspace-envs",
cardSelector: ".env-config-card",
nameSelector: ".env-config-name",
fallback: "Environment"
},
{
label: "Profiles",
module: "profiles",
containerSelector: ".workspace-profiles",
cardSelector: ".profile-card",
nameSelector: ".profile-name",
fallback: "Profile"
},
{
label: "Tasks",
module: "tasks",
containerSelector: ".workspace-tasks",
cardSelector: ".task-card",
nameSelector: ".task-name",
fallback: "Task"
},
{
label: "Toolbar Shortcuts",
module: "shortcuts",
containerSelector: ".workspace-shortcuts",
cardSelector: ".shortcut-card",
nameSelector: ".shortcut-name",
fallback: "Shortcut"
},
{ label: "Sites" }
];
for (const section of sectionConfigs) {
const subLi = document.createElement("li");
const link = document.createElement("a");
link.textContent = section.label;
link.href = "#";
const sectionKey = section.module
? `workspace:${ws.id}:${section.module}`
: section.label === "Appearance"
? `workspace:${ws.id}:appearance`
: section.label === "API Configurations"
? `workspace:${ws.id}:apiConfigs`
: section.label === "Sites"
? `workspace:${ws.id}:sites`
: "";
if (sectionKey) {
link.dataset.tocTargetSelector = `.workspace-card[data-id="${ws.id}"] details[data-state-key="${sectionKey}"]`;
}
link.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const tocDetails = link.closest("details");
if (tocDetails && !tocDetails.open) {
openDetails(tocDetails);
}
const card = document.querySelector(`.workspace-card[data-id="${ws.id}"]`);
if (card) {
const details = [...card.querySelectorAll("details")].find((d) => {
const heading = d.querySelector(".panel-summary h3, .panel-summary h2");
return heading && heading.textContent.trim() === section.label;
});
if (details) {
openDetailsChain(details);
details.scrollIntoView({ behavior: "smooth", block: "start" });
} else {
card.scrollIntoView({ behavior: "smooth", block: "start" });
openDetailsChain(document.getElementById("workspaces-panel"));
}
}
});
if (section.module) {
const details = document.createElement("details");
details.className = "toc-group toc-section";
details.dataset.stateKey = `toc:workspace:${ws.id}:${section.module}`;
const summary = document.createElement("summary");
summary.appendChild(link);
details.appendChild(summary);
const card = document.querySelector(`.workspace-card[data-id="${ws.id}"]`);
const container = card?.querySelector(section.containerSelector);
const list = document.createElement("ul");
list.className = "toc-sub toc-cards";
renderTocCardList(
list,
container?.querySelectorAll(section.cardSelector) || [],
section.nameSelector,
section.fallback,
(target) => {
openDetailsChain(target);
centerCardInView(target);
}
);
details.appendChild(list);
subLi.appendChild(details);
registerDetail(details, false);
} else {
subLi.appendChild(link);
}
subUl.appendChild(subLi);
}
a.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const card = document.querySelector(`.workspace-card[data-id="${ws.id}"]`);
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "start" });
openDetailsChain(document.getElementById("workspaces-panel"));
}
openDetails(details);
});
details.appendChild(subUl);
li.appendChild(details);
wsList.appendChild(li);
registerDetail(details, false);
}
const sitesList = document.getElementById("toc-sites-list");
if (sitesList) {
const existingSiteGroups = sitesList.querySelectorAll("details.toc-group");
existingSiteGroups.forEach((group) => {
const key = getDetailStateKey(group);
if (!key) return;
settingsViewState.open[key] = group.open;
});
scheduleSettingsViewStateSave();
sitesList.innerHTML = "";
for (const site of sites) {
const li = document.createElement("li");
const details = document.createElement("details");
details.className = "toc-group toc-site";
details.dataset.stateKey = `toc:site:${site.id}`;
const summary = document.createElement("summary");
const a = document.createElement("a");
a.textContent = site.name || site.urlPattern || "Untitled Site";
a.href = "#";
a.dataset.tocTargetSelector = `.site-card[data-id="${site.id}"]`;
summary.appendChild(a);
details.appendChild(summary);
const subUl = document.createElement("ul");
subUl.className = "toc-sub";
const sectionConfigs = [
{ label: "Appearance" },
{ label: "API Configurations" },
{
label: "Environments",
module: "envs",
containerSelector: ".site-envs",
cardSelector: ".env-config-card",
nameSelector: ".env-config-name",
fallback: "Environment"
},
{
label: "Profiles",
module: "profiles",
containerSelector: ".site-profiles",
cardSelector: ".profile-card",
nameSelector: ".profile-name",
fallback: "Profile"
},
{
label: "Tasks",
module: "tasks",
containerSelector: ".site-tasks",
cardSelector: ".task-card",
nameSelector: ".task-name",
fallback: "Task"
},
{
label: "Toolbar Shortcuts",
module: "shortcuts",
containerSelector: ".site-shortcuts",
cardSelector: ".shortcut-card",
nameSelector: ".shortcut-name",
fallback: "Shortcut"
}
];
for (const section of sectionConfigs) {
const subLi = document.createElement("li");
const link = document.createElement("a");
link.textContent = section.label;
link.href = "#";
const sectionKey = section.module
? `site:${site.id}:${section.module}`
: section.label === "Appearance"
? `site:${site.id}:appearance`
: section.label === "API Configurations"
? `site:${site.id}:apiConfigs`
: "";
if (sectionKey) {
link.dataset.tocTargetSelector = `.site-card[data-id="${site.id}"] details[data-state-key="${sectionKey}"]`;
}
link.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const tocDetails = link.closest("details");
if (tocDetails && !tocDetails.open) {
openDetails(tocDetails);
}
const card = document.querySelector(`.site-card[data-id="${site.id}"]`);
if (card) {
const detailsMatch = [...card.querySelectorAll("details")].find((d) => {
const heading = d.querySelector(".panel-summary h3, .panel-summary h2");
return heading && heading.textContent.trim() === section.label;
});
if (detailsMatch) {
openDetailsChain(detailsMatch);
detailsMatch.scrollIntoView({ behavior: "smooth", block: "start" });
} else {
card.scrollIntoView({ behavior: "smooth", block: "start" });
openDetailsChain(document.getElementById("sites-panel"));
}
}
});
if (section.module) {
const details = document.createElement("details");
details.className = "toc-group toc-section";
details.dataset.stateKey = `toc:site:${site.id}:${section.module}`;
const summary = document.createElement("summary");
summary.appendChild(link);
details.appendChild(summary);
const card = document.querySelector(`.site-card[data-id="${site.id}"]`);
const container = card?.querySelector(section.containerSelector);
const list = document.createElement("ul");
list.className = "toc-sub toc-cards";
renderTocCardList(
list,
container?.querySelectorAll(section.cardSelector) || [],
section.nameSelector,
section.fallback,
(target) => {
openDetailsChain(target);
centerCardInView(target);
}
);
details.appendChild(list);
subLi.appendChild(details);
registerDetail(details, false);
} else {
subLi.appendChild(link);
}
subUl.appendChild(subLi);
}
a.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const card = document.querySelector(`.site-card[data-id="${site.id}"]`);
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
openDetailsChain(document.getElementById("sites-panel"));
}
openDetails(details);
});
details.appendChild(subUl);
li.appendChild(details);
sitesList.appendChild(li);
registerDetail(details, false);
}
}
const globalTocSections = document.querySelectorAll(".toc-global-section");
globalTocSections.forEach((section) => {
registerDetail(section, section.open);
});
renderTocCardList(
document.getElementById("toc-global-envs-list"),
envConfigsContainer?.querySelectorAll(".env-config-card"),
".env-config-name",
"Environment",
(card) => {
openDetailsChain(card);
centerCardInView(card);
}
);
renderTocCardList(
document.getElementById("toc-global-profiles-list"),
profilesContainer?.querySelectorAll(".profile-card"),
".profile-name",
"Profile",
(card) => {
openDetailsChain(card);
centerCardInView(card);
}
);
renderTocCardList(
document.getElementById("toc-global-tasks-list"),
tasksContainer?.querySelectorAll(".task-card"),
".task-name",
"Task",
(card) => {
openDetailsChain(card);
centerCardInView(card);
}
);
renderTocCardList(
document.getElementById("toc-global-shortcuts-list"),
shortcutsContainer?.querySelectorAll(".shortcut-card"),
".shortcut-name",
"Shortcut",
(card) => {
openDetailsChain(card);
centerCardInView(card);
}
);
renderTocCardList(
document.getElementById("toc-global-api-keys-list"),
apiKeysContainer?.querySelectorAll(".api-key-card"),
".api-key-name",
"API Key",
(card) => {
openDetailsChain(card);
centerCardInView(card);
}
);
renderTocCardList(
document.getElementById("toc-global-api-configs-list"),
apiConfigsContainer?.querySelectorAll(".api-config-card"),
".api-config-name",
"API Config",
(card) => {
openDetailsChain(card);
centerCardInView(card);
}
);
const workspaceCards = document.querySelectorAll(".workspace-card");
workspaceCards.forEach((card) => {
const list = card.querySelector(".workspace-sites-list");
if (!list) return;
renderWorkspaceSitesList(list, card.dataset.id, sites);
});
refreshTocTargets();
}
function initToc() {
const tocGroups = document.querySelectorAll(".toc-links .toc-group");
tocGroups.forEach((group, index) => {
if (!group.dataset.stateKey) {
group.dataset.stateKey = `toc-group:${index}`;
}
registerDetail(group, group.open);
});
const links = document.querySelectorAll(".toc-links a[href^=\"#\"]");
links.forEach((link) => {
const href = link.getAttribute("href");
if (!href || href === "#") return;
const isSummaryLink = Boolean(link.closest("summary"));
link.addEventListener("click", (e) => {
const target = document.querySelector(href);
const tocDetails = link.closest("details");
if (isSummaryLink && tocDetails && !tocDetails.open) {
openDetails(tocDetails);
}
if (target) {
openDetailsChain(target);
target.scrollIntoView({ behavior: "smooth", block: "start" });
}
e.preventDefault();
e.stopPropagation();
});
});
refreshTocTargets();
}
function handleSettingsInputChange() {
scheduleSidebarErrors();
scheduleDirtyCheck();
refreshInheritedSourceLabels();
updateAppearanceInheritanceIndicators();
}
document.addEventListener("input", handleSettingsInputChange);
document.addEventListener("change", handleSettingsInputChange);