diff --git a/sitecompanion/background.js b/sitecompanion/background.js index f6a57fe..3cf6826 100644 --- a/sitecompanion/background.js +++ b/sitecompanion/background.js @@ -29,12 +29,15 @@ const DEFAULT_SETTINGS = { systemPrompt: "You are a precise, honest assistant. Be concise and avoid inventing details, be critical about evaluations. You should put in a small summary of all the sections at the end. You should answer in no longer than 3 sections including the summary. And remember to bold or italicize key points.", tasks: DEFAULT_TASKS, + shortcuts: [], theme: "system", + toolbarAutoHide: true, workspaces: [] }; const OUTPUT_STORAGE_KEY = "lastOutput"; const AUTO_RUN_KEY = "autoRunDefaultTask"; +const SHORTCUT_RUN_KEY = "runShortcutId"; let activeAbortController = null; let keepalivePort = null; const streamState = { @@ -354,6 +357,16 @@ chrome.runtime.onConnect.addListener((port) => { }); chrome.runtime.onMessage.addListener((message) => { + if (message?.type === "RUN_SHORTCUT") { + const shortcutId = message.shortcutId || ""; + if (shortcutId) { + void chrome.storage.local.set({ [SHORTCUT_RUN_KEY]: shortcutId }); + if (chrome.action?.openPopup) { + void chrome.action.openPopup().catch(() => {}); + } + } + return; + } if (message?.type !== "RUN_DEFAULT_TASK") return; void chrome.storage.local.set({ [AUTO_RUN_KEY]: Date.now() }); if (chrome.action?.openPopup) { diff --git a/sitecompanion/content.js b/sitecompanion/content.js index 350a6cd..f9d2e2b 100644 --- a/sitecompanion/content.js +++ b/sitecompanion/content.js @@ -22,7 +22,32 @@ function findMinimumScope(text) { return deepest; } -function createToolbar(presets, position = "bottom-right") { +function normalizeName(value) { + return (value || "").trim().toLowerCase(); +} + +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 (item?.enabled === false) 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 localEnabled = local.filter((item) => item?.enabled !== false); + return [...inherited, ...localEnabled]; +} + +function createToolbar(shortcuts, position = "bottom-right") { let toolbar = document.getElementById("sitecompanion-toolbar"); if (toolbar) toolbar.remove(); @@ -63,16 +88,16 @@ function createToolbar(presets, position = "bottom-right") { font-family: system-ui, sans-serif; `; - if (!presets || !presets.length) { + if (!shortcuts || !shortcuts.length) { const label = document.createElement("span"); label.textContent = "SiteCompanion"; label.style.fontSize = "12px"; label.style.color = "#6b5f55"; toolbar.appendChild(label); } else { - for (const preset of presets) { + for (const shortcut of shortcuts) { const btn = document.createElement("button"); - btn.textContent = preset.name; + btn.textContent = shortcut.name; btn.style.cssText = ` padding: 6px 12px; background: #b14d2b; @@ -83,7 +108,7 @@ function createToolbar(presets, position = "bottom-right") { font-size: 12px; `; btn.addEventListener("click", () => { - chrome.runtime.sendMessage({ type: "RUN_PRESET", presetId: preset.id }); + chrome.runtime.sendMessage({ type: "RUN_SHORTCUT", shortcutId: shortcut.id }); }); toolbar.appendChild(btn); } @@ -95,36 +120,68 @@ function createToolbar(presets, position = "bottom-right") { function matchUrl(url, pattern) { - if (!pattern) return false; - - const regex = new RegExp("^" + pattern.split("*").join(".*") + "$"); - + let regex = null; try { - - const urlObj = new URL(url); - - const target = urlObj.hostname + urlObj.pathname; - - return regex.test(target); - + regex = new RegExp("^" + pattern.split("*").join(".*") + "$"); + } catch { + return false; + } + try { + const urlObj = new URL(url); + const target = urlObj.hostname + urlObj.pathname; + return regex.test(target); } catch { - return false; - } - } async function refreshToolbar() { - const { sites = [], presets = [], toolbarPosition = "bottom-right" } = await chrome.storage.local.get(["sites", "presets", "toolbarPosition"]); + let { + sites = [], + workspaces = [], + shortcuts = [], + presets = [], + toolbarPosition = "bottom-right" + } = await chrome.storage.local.get([ + "sites", + "workspaces", + "shortcuts", + "presets", + "toolbarPosition" + ]); const currentUrl = window.location.href; const site = sites.find(s => matchUrl(currentUrl, s.urlPattern)); if (site) { - createToolbar(presets, toolbarPosition); + if (!shortcuts.length && Array.isArray(presets) && presets.length) { + shortcuts = presets; + await chrome.storage.local.set({ shortcuts }); + await chrome.storage.local.remove("presets"); + } + const workspace = + workspaces.find((ws) => ws.id === site.workspaceId) || null; + const workspaceDisabled = workspace?.disabledInherited?.shortcuts || []; + const siteDisabled = site?.disabledInherited?.shortcuts || []; + const workspaceShortcuts = resolveScopedItems( + shortcuts, + workspace?.shortcuts || [], + workspaceDisabled + ); + const siteShortcuts = resolveScopedItems( + workspaceShortcuts, + site.shortcuts || [], + siteDisabled + ); + const resolvedPosition = + site.toolbarPosition && site.toolbarPosition !== "inherit" + ? site.toolbarPosition + : workspace?.toolbarPosition && workspace.toolbarPosition !== "inherit" + ? workspace.toolbarPosition + : toolbarPosition; + createToolbar(siteShortcuts, resolvedPosition); } } @@ -140,4 +197,25 @@ const observer = new MutationObserver(() => { // observer.observe(document.documentElement, { childList: true, subtree: true }); -refreshToolbar(); +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (!message || typeof message !== "object") return; + if (message.type === "FIND_SCOPE") { + const node = findMinimumScope(message.text || ""); + if (!node) { + sendResponse({ ok: false, error: "Scope not found." }); + return; + } + sendResponse({ ok: true, extracted: node.innerText || "" }); + return; + } + if (message.type === "EXTRACT_FULL") { + const extracted = document.body?.innerText || ""; + sendResponse({ ok: true, extracted }); + } +}); + +try { + refreshToolbar(); +} catch (error) { + console.warn("SiteCompanion toolbar failed:", error); +} diff --git a/sitecompanion/popup.js b/sitecompanion/popup.js index 7f4e607..f6b89e8 100644 --- a/sitecompanion/popup.js +++ b/sitecompanion/popup.js @@ -14,6 +14,7 @@ const clearOutputBtn = document.getElementById("clearOutputBtn"); const OUTPUT_STORAGE_KEY = "lastOutput"; const AUTO_RUN_KEY = "autoRunDefaultTask"; +const SHORTCUT_RUN_KEY = "runShortcutId"; const LAST_TASK_KEY = "lastSelectedTaskId"; const LAST_ENV_KEY = "lastSelectedEnvId"; const LAST_PROFILE_KEY = "lastSelectedProfileId"; @@ -42,6 +43,7 @@ const state = { isAnalyzing: false, outputRaw: "", autoRunPending: false, + shortcutRunPending: false, selectedTaskId: "", selectedEnvId: "", selectedProfileId: "" @@ -64,7 +66,12 @@ async function switchState(stateName) { function matchUrl(url, pattern) { if (!pattern) return false; - const regex = new RegExp("^" + pattern.split("*").join(".*") + "$"); + let regex = null; + try { + regex = new RegExp("^" + pattern.split("*").join(".*") + "$"); + } catch { + return false; + } try { const urlObj = new URL(url); const target = urlObj.hostname + urlObj.pathname; @@ -74,6 +81,62 @@ function matchUrl(url, pattern) { } } +function normalizeName(value) { + return (value || "").trim().toLowerCase(); +} + +function normalizeConfigList(list) { + return Array.isArray(list) + ? list.map((item) => ({ ...item, enabled: item.enabled !== false })) + : []; +} + +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 (item?.enabled === false) 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 localEnabled = local.filter((item) => item?.enabled !== false); + return [...inherited, ...localEnabled]; +} + +function resolveEffectiveList(globalItems, workspace, site, listKey, disabledKey) { + const workspaceItems = workspace?.[listKey] || []; + const workspaceDisabled = workspace?.disabledInherited?.[disabledKey] || []; + const workspaceEffective = resolveScopedItems( + globalItems, + workspaceItems, + workspaceDisabled + ); + const siteItems = site?.[listKey] || []; + const siteDisabled = site?.disabledInherited?.[disabledKey] || []; + return resolveScopedItems(workspaceEffective, siteItems, siteDisabled); +} + +function filterApiConfigsForScope(apiConfigs, workspace, site) { + const workspaceDisabled = workspace?.disabledInherited?.apiConfigs || []; + const siteDisabled = site?.disabledInherited?.apiConfigs || []; + const workspaceFiltered = apiConfigs.filter( + (config) => + config?.enabled !== false && !workspaceDisabled.includes(config.id) + ); + return workspaceFiltered.filter( + (config) => !siteDisabled.includes(config.id) + ); +} + async function detectSite(url) { const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]); state.sites = sites; @@ -82,7 +145,13 @@ async function detectSite(url) { const site = sites.find(s => matchUrl(url, s.urlPattern)); if (site) { state.currentSite = site; - state.currentWorkspace = workspaces.find(w => w.id === site.workspaceId) || { name: "Global", id: "global" }; + const workspace = + workspaces.find((entry) => entry.id === site.workspaceId) || null; + state.currentWorkspace = workspace || { + name: "Global", + id: "global", + disabledInherited: {} + }; currentWorkspaceName.textContent = state.currentWorkspace.name; switchState("normal"); return true; @@ -552,40 +621,88 @@ async function loadConfig() { "tasks", "envConfigs", "profiles", + "shortcuts", + "workspaces", + "sites", LAST_TASK_KEY, LAST_ENV_KEY, LAST_PROFILE_KEY ]); - const tasks = Array.isArray(stored.tasks) ? stored.tasks : []; - const envs = Array.isArray(stored.envConfigs) ? stored.envConfigs : []; - const profiles = Array.isArray(stored.profiles) ? stored.profiles : []; - renderTasks(tasks); - renderEnvironments(envs); - renderProfiles(profiles); + const tasks = normalizeConfigList(stored.tasks); + const envs = normalizeConfigList(stored.envConfigs); + const profiles = normalizeConfigList(stored.profiles); + const shortcuts = normalizeConfigList(stored.shortcuts); + const sites = Array.isArray(stored.sites) ? stored.sites : state.sites; + const workspaces = Array.isArray(stored.workspaces) + ? stored.workspaces + : state.workspaces; + state.sites = sites; + state.workspaces = workspaces; - if (!tasks.length) { + const activeSite = state.currentSite + ? sites.find((entry) => entry.id === state.currentSite.id) + : null; + const activeWorkspace = + activeSite && activeSite.workspaceId + ? workspaces.find((entry) => entry.id === activeSite.workspaceId) + : null; + if (activeWorkspace) { + state.currentWorkspace = activeWorkspace; + currentWorkspaceName.textContent = activeWorkspace.name || "Global"; + } + + const effectiveEnvs = resolveEffectiveList( + envs, + activeWorkspace, + activeSite, + "envConfigs", + "envs" + ); + const effectiveProfiles = resolveEffectiveList( + profiles, + activeWorkspace, + activeSite, + "profiles", + "profiles" + ); + const effectiveTasks = resolveEffectiveList( + tasks, + activeWorkspace, + activeSite, + "tasks", + "tasks" + ); + + renderTasks(effectiveTasks); + renderEnvironments(effectiveEnvs); + renderProfiles(effectiveProfiles); + + if (!effectiveTasks.length) { state.selectedTaskId = ""; - setEnvironmentSelection(envs[0]?.id || ""); - setProfileSelection(profiles[0]?.id || ""); + setEnvironmentSelection(effectiveEnvs[0]?.id || ""); + setProfileSelection(effectiveProfiles[0]?.id || ""); return; } const storedTaskId = stored[LAST_TASK_KEY]; const storedEnvId = stored[LAST_ENV_KEY]; const storedProfileId = stored[LAST_PROFILE_KEY]; - const initialTaskId = tasks.some((task) => task.id === storedTaskId) + const initialTaskId = effectiveTasks.some((task) => task.id === storedTaskId) ? storedTaskId - : tasks[0].id; + : effectiveTasks[0].id; selectTask(initialTaskId, { resetEnv: false }); - const task = tasks.find((item) => item.id === initialTaskId); - if (storedEnvId && envs.some((env) => env.id === storedEnvId)) { + const task = effectiveTasks.find((item) => item.id === initialTaskId); + if (storedEnvId && effectiveEnvs.some((env) => env.id === storedEnvId)) { setEnvironmentSelection(storedEnvId); } else { setEnvironmentSelection(getTaskDefaultEnvId(task)); } - if (storedProfileId && profiles.some((profile) => profile.id === storedProfileId)) { + if ( + storedProfileId && + effectiveProfiles.some((profile) => profile.id === storedProfileId) + ) { setProfileSelection(storedProfileId); } else { setProfileSelection(getTaskDefaultProfileId(task)); @@ -652,8 +769,6 @@ async function handleAnalyze() { activeApiKeyId = "", apiConfigs = [], activeApiConfigId = "", - envConfigs = [], - profiles = [], apiBaseUrl, apiKeyHeader, apiKeyPrefix, @@ -665,8 +780,6 @@ async function handleAnalyze() { "activeApiKeyId", "apiConfigs", "activeApiConfigId", - "envConfigs", - "profiles", "apiBaseUrl", "apiKeyHeader", "apiKeyPrefix", @@ -675,9 +788,13 @@ async function handleAnalyze() { "resume" ]); - const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : []; - const resolvedEnvs = Array.isArray(envConfigs) ? envConfigs : []; - const resolvedProfiles = Array.isArray(profiles) ? profiles : []; + const resolvedConfigs = filterApiConfigsForScope( + normalizeConfigList(apiConfigs), + state.currentWorkspace, + state.currentSite + ); + const resolvedEnvs = Array.isArray(state.envs) ? state.envs : []; + const resolvedProfiles = Array.isArray(state.profiles) ? state.profiles : []; const selectedEnvId = envSelect.value; const activeEnv = resolvedEnvs.find((entry) => entry.id === selectedEnvId) || @@ -711,7 +828,9 @@ async function handleAnalyze() { const resolvedApiKeyPrefix = activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? ""; const resolvedModel = activeConfig?.model || model || ""; - const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : []; + const resolvedKeys = normalizeConfigList(apiKeys).filter( + (key) => key.enabled !== false + ); const resolvedKeyId = activeConfig?.apiKeyId || activeApiKeyId || resolvedKeys[0]?.id || ""; const activeKey = resolvedKeys.find((entry) => entry.id === resolvedKeyId); @@ -926,8 +1045,7 @@ updateSiteTextCount(); updatePromptCount(0); renderOutput(); setAnalyzing(false); -loadConfig(); -loadTheme(); +void loadTheme(); async function loadSavedOutput() { const stored = await getStorage([OUTPUT_STORAGE_KEY]); @@ -935,6 +1053,60 @@ async function loadSavedOutput() { renderOutput(); } +async function loadShortcutRunRequest() { + const stored = await getStorage([ + SHORTCUT_RUN_KEY, + "shortcuts", + "workspaces", + "sites" + ]); + const shortcutId = stored[SHORTCUT_RUN_KEY]; + if (!shortcutId) return; + + state.shortcutRunPending = true; + await chrome.storage.local.remove(SHORTCUT_RUN_KEY); + + const globalShortcuts = normalizeConfigList(stored.shortcuts); + const sites = Array.isArray(stored.sites) ? stored.sites : state.sites; + const workspaces = Array.isArray(stored.workspaces) + ? stored.workspaces + : state.workspaces; + const activeSite = state.currentSite + ? sites.find((entry) => entry.id === state.currentSite.id) + : null; + const activeWorkspace = + activeSite && activeSite.workspaceId + ? workspaces.find((entry) => entry.id === activeSite.workspaceId) + : null; + const effectiveShortcuts = resolveEffectiveList( + globalShortcuts, + activeWorkspace, + activeSite, + "shortcuts", + "shortcuts" + ); + const shortcut = effectiveShortcuts.find((item) => item.id === shortcutId); + if (!shortcut) { + setStatus("Shortcut not found."); + state.shortcutRunPending = false; + return; + } + + if (shortcut.taskId) { + selectTask(shortcut.taskId, { resetEnv: true }); + } + if (shortcut.envId) { + setEnvironmentSelection(shortcut.envId); + } + if (shortcut.profileId) { + setProfileSelection(shortcut.profileId); + } + await persistSelections(); + state.autoRunPending = false; + state.shortcutRunPending = false; + void handleExtractAndAnalyze(); +} + async function loadAutoRunRequest() { const stored = await getStorage([AUTO_RUN_KEY]); if (stored[AUTO_RUN_KEY]) { @@ -945,6 +1117,7 @@ async function loadAutoRunRequest() { } function maybeRunDefaultTask() { + if (state.shortcutRunPending) return; if (!state.autoRunPending) return; if (state.isAnalyzing) return; if (!state.tasks.length) return; @@ -954,8 +1127,14 @@ function maybeRunDefaultTask() { void handleExtractAndAnalyze(); } -loadSavedOutput(); -loadAutoRunRequest(); +async function init() { + await loadConfig(); + await loadShortcutRunRequest(); + await loadAutoRunRequest(); +} + +void init(); +void loadSavedOutput(); ensurePort(); chrome.storage.onChanged.addListener((changes) => { @@ -965,6 +1144,10 @@ chrome.storage.onChanged.addListener((changes) => { maybeRunDefaultTask(); } + if (changes[SHORTCUT_RUN_KEY]?.newValue) { + void loadShortcutRunRequest(); + } + if (changes[OUTPUT_STORAGE_KEY]?.newValue !== undefined) { if (!state.isAnalyzing || !state.port) { state.outputRaw = changes[OUTPUT_STORAGE_KEY].newValue || ""; diff --git a/sitecompanion/settings.css b/sitecompanion/settings.css index a1969af..8cd78ea 100644 --- a/sitecompanion/settings.css +++ b/sitecompanion/settings.css @@ -199,38 +199,31 @@ body { overflow: hidden; } -.panel-summary { - list-style: none; - cursor: pointer; - display: flex; - align-items: baseline; - justify-content: flex-start; - gap: 12px; - padding: 12px 16px; - margin: 0; -} - -/* Restore native marker but keep list-style: none from above? No, remove list-style: none to show marker. */ -/* Wait, display: flex might hide marker in some browsers. */ -/* Usually marker is ::marker pseudo-element on summary. */ -/* To show native marker, summary should be display: list-item or similar? */ -/* Actually, standard is display: block (or list-item). Flex might kill it. */ -/* If the user wants native glyphs, I should use list-item and maybe position the h2? */ - -/* Let's try reverting panel-summary to default display and styling h2 inline. */ - .panel-summary { cursor: pointer; padding: 12px 16px; margin: 0; - /* display: list-item; default */ + display: list-item; + list-style: revert; + list-style-position: inside; } -/* Need to align H2. */ -.panel-summary h2 { +.panel-summary::marker { + color: var(--muted); +} + +.panel-summary h2, +.panel-summary h3 { display: inline; } +.panel-summary .row-title { + display: inline-flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; +} + .sub-panel .panel-summary { padding: 10px 12px; } @@ -312,6 +305,20 @@ label { color: var(--muted); } +.toggle-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + text-transform: none; + letter-spacing: 0; + color: var(--muted); +} + +.toggle-label input[type="checkbox"] { + width: auto; +} + input, textarea, select { @@ -378,6 +385,109 @@ button:active { gap: 8px; } +.shortcuts { + display: grid; + gap: 12px; +} + +.shortcut-card { + padding: 12px; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--card-bg); + display: grid; + gap: 8px; +} + +.scope-group { + margin-top: 12px; + display: grid; + gap: 8px; +} + +.scope-title { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--muted); +} + +.inherited-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.inherited-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.inherited-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 10px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--panel); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--muted); + cursor: pointer; +} + +.inherited-button input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.inherited-item.is-enabled .inherited-button { + border: 1px solid var(--accent); + background: var(--accent); + color: #fff9f3; + box-shadow: 0 8px 20px rgba(177, 77, 43, 0.2); +} + +.inherited-item.is-disabled .inherited-button { + background: transparent; + color: var(--muted); +} + +.inherited-item.is-overridden .inherited-button { + cursor: default; + opacity: 0.65; +} + +.dup-controls { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.dup-select { + min-width: 160px; +} + +.sites-list { + display: grid; + gap: 6px; + font-size: 12px; +} + +.sites-list a { + color: var(--muted); + text-decoration: none; +} + +.sites-list a:hover { + color: var(--ink); +} + .api-keys { display: grid; gap: 12px; diff --git a/sitecompanion/settings.html b/sitecompanion/settings.html index cc91433..0408bc4 100644 --- a/sitecompanion/settings.html +++ b/sitecompanion/settings.html @@ -13,7 +13,7 @@
- +
@@ -28,27 +28,28 @@
Global Configuration
-
@@ -84,6 +85,12 @@ +
+ +
@@ -168,20 +175,33 @@ - -
+ +
-

PRESETS

- Toolbar shortcuts +

TOOLBAR SHORTCUTS

+ One-click toolbar runs
- +
-
+
+
+
+ + +
+ +
+

SITES

+ Inherit directly from global +
+
+
+
diff --git a/sitecompanion/settings.js b/sitecompanion/settings.js index 8ee4a76..f6ce166 100644 --- a/sitecompanion/settings.js +++ b/sitecompanion/settings.js @@ -14,13 +14,15 @@ const addWorkspaceBtn = document.getElementById("addWorkspaceBtn"); const workspacesContainer = document.getElementById("workspaces"); const addSiteBtn = document.getElementById("addSiteBtn"); const sitesContainer = document.getElementById("sites"); -const addPresetBtn = document.getElementById("addPresetBtn"); -const presetsContainer = document.getElementById("presets"); +const addShortcutBtn = document.getElementById("addShortcutBtn"); +const shortcutsContainer = document.getElementById("shortcuts"); const statusEl = document.getElementById("status"); const statusSidebarEl = document.getElementById("statusSidebar"); const sidebarErrorsEl = document.getElementById("sidebarErrors"); const themeSelect = document.getElementById("themeSelect"); const toolbarPositionSelect = document.getElementById("toolbarPositionSelect"); +const toolbarAutoHide = document.getElementById("toolbarAutoHide"); +const globalSitesContainer = document.getElementById("globalSites"); const OPENAI_DEFAULTS = { apiBaseUrl: "https://api.openai.com/v1", @@ -55,6 +57,35 @@ function scheduleSidebarErrors() { }); } +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.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 applyTheme(theme) { const value = theme || "system"; document.documentElement.dataset.theme = value; @@ -95,9 +126,9 @@ function newSiteId() { return `site-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; } -function newPresetId() { +function newShortcutId() { if (crypto?.randomUUID) return crypto.randomUUID(); - return `preset-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; + return `shortcut-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; } function buildChatUrlFromBase(baseUrl) { @@ -133,12 +164,62 @@ function ensureUniqueName(desired, existingNames) { 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 normalizeConfigList(list) { + return Array.isArray(list) + ? list.map((item) => ({ ...item, enabled: item.enabled !== 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()[0]?.id || ""; + return collectEnvConfigs().find((env) => isEnabled(env.enabled))?.id || ""; } function getTopProfileId() { - return collectProfiles()[0]?.id || ""; + return collectProfiles().find((profile) => isEnabled(profile.enabled))?.id || ""; } function setApiConfigAdvanced(card, isAdvanced) { @@ -174,6 +255,7 @@ function readApiConfigFromCard(card) { 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 { @@ -186,7 +268,8 @@ function readApiConfigFromCard(card) { model: (modelInput?.value || "").trim(), apiUrl: (urlInput?.value || "").trim(), requestTemplate: (templateInput?.value || "").trim(), - advanced: isAdvanced + advanced: isAdvanced, + enabled: enabledInput ? enabledInput.checked : true }; } @@ -196,6 +279,18 @@ function buildApiConfigCard(config) { card.dataset.id = config.id || newApiConfigId(); const isAdvanced = Boolean(config.advanced); + const enabledLabel = document.createElement("label"); + enabledLabel.className = "toggle-label"; + const enabledInput = document.createElement("input"); + enabledInput.type = "checkbox"; + enabledInput.className = "config-enabled"; + enabledInput.checked = config.enabled !== false; + enabledInput.addEventListener("change", () => { + updateTaskEnvOptions(); + }); + enabledLabel.appendChild(enabledInput); + enabledLabel.appendChild(document.createTextNode("Enabled")); + const nameField = document.createElement("div"); nameField.className = "field"; const nameLabel = document.createElement("label"); @@ -387,20 +482,6 @@ function buildApiConfigCard(config) { setApiConfigAdvanced(card, true); updateEnvApiOptions(); }); - const duplicateBtn = document.createElement("button"); - duplicateBtn.type = "button"; - duplicateBtn.className = "ghost duplicate"; - duplicateBtn.textContent = "Duplicate"; - duplicateBtn.addEventListener("click", () => { - const names = collectNames(apiConfigsContainer, ".api-config-name"); - const copy = readApiConfigFromCard(card); - copy.id = newApiConfigId(); - copy.name = ensureUniqueName(`${copy.name || "Default"} Copy`, names); - const newCard = buildApiConfigCard(copy); - card.insertAdjacentElement("afterend", newCard); - updateApiConfigKeyOptions(); - updateEnvApiOptions(); - }); const resetBtn = document.createElement("button"); resetBtn.type = "button"; resetBtn.className = "ghost reset-openai"; @@ -438,7 +519,6 @@ function buildApiConfigCard(config) { rightActions.appendChild(moveUpBtn); rightActions.appendChild(moveDownBtn); rightActions.appendChild(addBelowBtn); - rightActions.appendChild(duplicateBtn); rightActions.appendChild(deleteBtn); leftActions.appendChild(advancedBtn); @@ -447,6 +527,7 @@ function buildApiConfigCard(config) { actions.appendChild(leftActions); actions.appendChild(rightActions); + card.appendChild(enabledLabel); card.appendChild(nameField); card.appendChild(keyField); card.appendChild(baseField); @@ -485,6 +566,18 @@ function buildApiKeyCard(entry) { card.className = "api-key-card"; card.dataset.id = entry.id || newApiKeyId(); + const enabledLabel = document.createElement("label"); + enabledLabel.className = "toggle-label"; + const enabledInput = document.createElement("input"); + enabledInput.type = "checkbox"; + enabledInput.className = "config-enabled"; + enabledInput.checked = entry.enabled !== false; + enabledInput.addEventListener("change", () => { + updateApiConfigKeyOptions(); + }); + enabledLabel.appendChild(enabledInput); + enabledLabel.appendChild(document.createTextNode("Enabled")); + const nameField = document.createElement("div"); nameField.className = "field"; const nameLabel = document.createElement("label"); @@ -594,6 +687,7 @@ function buildApiKeyCard(entry) { nameInput.addEventListener("input", updateSelect); keyInput.addEventListener("input", updateSelect); + card.appendChild(enabledLabel); card.appendChild(nameField); card.appendChild(keyField); card.appendChild(actions); @@ -606,10 +700,12 @@ function collectApiKeys() { 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() + key: (keyInput?.value || "").trim(), + enabled: enabledInput ? enabledInput.checked : true }; }); } @@ -628,7 +724,7 @@ function updateApiKeyControls() { } function updateApiConfigKeyOptions() { - const keys = collectApiKeys(); + 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; @@ -660,11 +756,23 @@ function updateApiConfigKeyOptions() { }); } -function buildEnvConfigCard(config) { +function buildEnvConfigCard(config, container = envConfigsContainer) { const card = document.createElement("div"); card.className = "env-config-card"; card.dataset.id = config.id || newEnvConfigId(); + const enabledLabel = document.createElement("label"); + enabledLabel.className = "toggle-label"; + const enabledInput = document.createElement("input"); + enabledInput.type = "checkbox"; + enabledInput.className = "config-enabled"; + enabledInput.checked = config.enabled !== false; + enabledInput.addEventListener("change", () => { + updateEnvApiOptions(); + }); + enabledLabel.appendChild(enabledInput); + enabledLabel.appendChild(document.createTextNode("Enabled")); + const nameField = document.createElement("div"); nameField.className = "field"; const nameLabel = document.createElement("label"); @@ -717,26 +825,26 @@ function buildEnvConfigCard(config) { addBelowBtn.textContent = "Add"; moveTopBtn.addEventListener("click", () => { - const first = envConfigsContainer.firstElementChild; + const first = container.firstElementChild; if (!first || first === card) return; - envConfigsContainer.insertBefore(card, first); - updateEnvControls(); + container.insertBefore(card, first); + updateEnvControls(container); updateTaskEnvOptions(); }); moveUpBtn.addEventListener("click", () => { const previous = card.previousElementSibling; if (!previous) return; - envConfigsContainer.insertBefore(card, previous); - updateEnvControls(); + container.insertBefore(card, previous); + updateEnvControls(container); updateTaskEnvOptions(); }); moveDownBtn.addEventListener("click", () => { const next = card.nextElementSibling; if (!next) return; - envConfigsContainer.insertBefore(card, next.nextElementSibling); - updateEnvControls(); + container.insertBefore(card, next.nextElementSibling); + updateEnvControls(container); updateTaskEnvOptions(); }); @@ -747,44 +855,28 @@ function buildEnvConfigCard(config) { addBelowBtn.addEventListener("click", () => { const name = buildUniqueDefaultName( - collectNames(envConfigsContainer, ".env-config-name") + collectNames(container, ".env-config-name") ); - const fallbackApiConfigId = collectApiConfigs()[0]?.id || ""; + const fallbackApiConfigId = getApiConfigsForEnvContainer(container)[0]?.id || ""; const newCard = buildEnvConfigCard({ id: newEnvConfigId(), name, apiConfigId: fallbackApiConfigId, systemPrompt: DEFAULT_SYSTEM_PROMPT - }); + }, container); card.insertAdjacentElement("afterend", newCard); updateEnvApiOptions(); - updateEnvControls(); + updateEnvControls(container); updateTaskEnvOptions(); }); - const duplicateBtn = document.createElement("button"); - duplicateBtn.type = "button"; - duplicateBtn.className = "ghost duplicate"; - duplicateBtn.textContent = "Duplicate"; - duplicateBtn.addEventListener("click", () => { - const names = collectNames(envConfigsContainer, ".env-config-name"); - const copy = collectEnvConfigs().find((entry) => entry.id === card.dataset.id) || { - id: card.dataset.id, - name: nameInput.value || "Default", - apiConfigId: apiSelect.value || "", - systemPrompt: promptInput.value || "" - }; - const newCard = buildEnvConfigCard({ - id: newEnvConfigId(), - name: ensureUniqueName(`${copy.name || "Default"} Copy`, names), - apiConfigId: copy.apiConfigId, - systemPrompt: copy.systemPrompt - }); - card.insertAdjacentElement("afterend", newCard); - updateEnvApiOptions(); - updateEnvControls(); - updateTaskEnvOptions(); - }); + const duplicateControls = buildDuplicateControls("envs", () => ({ + id: card.dataset.id, + name: nameInput.value || "Default", + apiConfigId: apiSelect.value || "", + systemPrompt: promptInput.value || "", + enabled: enabledInput.checked + })); const deleteBtn = document.createElement("button"); deleteBtn.type = "button"; @@ -792,14 +884,15 @@ function buildEnvConfigCard(config) { deleteBtn.textContent = "Delete"; deleteBtn.addEventListener("click", () => { card.remove(); - updateEnvControls(); + updateEnvControls(container); updateTaskEnvOptions(); }); - actions.appendChild(duplicateBtn); + actions.appendChild(duplicateControls); actions.appendChild(deleteBtn); nameInput.addEventListener("input", () => updateEnvApiOptions()); + card.appendChild(enabledLabel); card.appendChild(nameField); card.appendChild(apiField); card.appendChild(promptField); @@ -808,8 +901,8 @@ function buildEnvConfigCard(config) { return card; } -function updateEnvControls() { - const cards = [...envConfigsContainer.querySelectorAll(".env-config-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"); @@ -821,37 +914,39 @@ function updateEnvControls() { scheduleSidebarErrors(); } -function updateTaskEnvOptions() { - const envs = collectEnvConfigs(); - const selects = tasksContainer.querySelectorAll(".task-env-select"); +function updateTaskEnvOptionsForContainer(container, envs) { + if (!container) return; + const selects = container.querySelectorAll(".task-env-select"); selects.forEach((select) => { - const preferred = select.dataset.preferred || select.value; - select.innerHTML = ""; - if (!envs.length) { - const option = document.createElement("option"); - option.value = ""; - option.textContent = "No environments configured"; - select.appendChild(option); - select.disabled = true; - return; - } - - select.disabled = false; - for (const env of envs) { - const option = document.createElement("option"); - option.value = env.id; - option.textContent = env.name || "Default"; - select.appendChild(option); - } - - if (preferred && envs.some((env) => env.id === preferred)) { - select.value = preferred; - } else { - select.value = envs[0].id; - } - - select.dataset.preferred = select.value; + populateSelect(select, envs, "No environments configured"); }); +} + +function updateTaskEnvOptions() { + const envs = collectEnvConfigs().filter((env) => isEnabled(env.enabled)); + updateTaskEnvOptionsForContainer(tasksContainer, envs); + + const workspaceCards = document.querySelectorAll(".workspace-card"); + workspaceCards.forEach((card) => { + const scope = getWorkspaceScopeData(card); + updateTaskEnvOptionsForContainer( + card.querySelector(".workspace-tasks"), + scope.envs + ); + }); + + const siteCards = document.querySelectorAll(".site-card"); + siteCards.forEach((card) => { + const scope = getSiteScopeData(card); + updateTaskEnvOptionsForContainer( + card.querySelector(".site-tasks"), + scope.envs + ); + }); + + updateShortcutOptions(); + refreshWorkspaceInheritedLists(); + refreshSiteInheritedLists(); scheduleSidebarErrors(); } @@ -860,6 +955,18 @@ function buildProfileCard(profile, container = profilesContainer) { card.className = "profile-card"; card.dataset.id = profile.id || newProfileId(); + const enabledLabel = document.createElement("label"); + enabledLabel.className = "toggle-label"; + const enabledInput = document.createElement("input"); + enabledInput.type = "checkbox"; + enabledInput.className = "config-enabled"; + enabledInput.checked = profile.enabled !== false; + enabledInput.addEventListener("change", () => { + updateTaskProfileOptions(); + }); + enabledLabel.appendChild(enabledInput); + enabledLabel.appendChild(document.createTextNode("Enabled")); + const nameField = document.createElement("div"); nameField.className = "field"; const nameLabel = document.createElement("label"); @@ -939,26 +1046,12 @@ function buildProfileCard(profile, container = profilesContainer) { updateTaskProfileOptions(); }); - const duplicateBtn = document.createElement("button"); - duplicateBtn.type = "button"; - duplicateBtn.className = "ghost duplicate"; - duplicateBtn.textContent = "Duplicate"; - duplicateBtn.addEventListener("click", () => { - const names = collectNames(container, ".profile-name"); - const copy = collectProfiles(container).find((entry) => entry.id === card.dataset.id) || { - id: card.dataset.id, - name: nameInput.value || "Default", - text: textArea.value || "" - }; - const newCard = buildProfileCard({ - id: newProfileId(), - name: ensureUniqueName(`${copy.name || "Default"} Copy`, names), - text: copy.text - }, container); - card.insertAdjacentElement("afterend", newCard); - updateProfileControls(container); - updateTaskProfileOptions(); - }); + const duplicateControls = buildDuplicateControls("profiles", () => ({ + id: card.dataset.id, + name: nameInput.value || "Default", + text: textArea.value || "", + enabled: enabledInput.checked + })); const deleteBtn = document.createElement("button"); deleteBtn.type = "button"; @@ -974,11 +1067,12 @@ function buildProfileCard(profile, container = profilesContainer) { actions.appendChild(moveUpBtn); actions.appendChild(moveDownBtn); actions.appendChild(addBelowBtn); - actions.appendChild(duplicateBtn); + actions.appendChild(duplicateControls); actions.appendChild(deleteBtn); nameInput.addEventListener("input", () => updateTaskProfileOptions()); + card.appendChild(enabledLabel); card.appendChild(nameField); card.appendChild(textField); card.appendChild(actions); @@ -987,14 +1081,17 @@ function buildProfileCard(profile, container = profilesContainer) { } 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() + text: (textArea?.value || "").trim(), + enabled: enabledInput ? enabledInput.checked : true }; }); } @@ -1012,85 +1109,334 @@ function updateProfileControls(container = profilesContainer) { scheduleSidebarErrors(); } -function updateTaskProfileOptions() { - const profiles = collectProfiles(); - const selects = tasksContainer.querySelectorAll(".task-profile-select"); +function updateTaskProfileOptionsForContainer(container, profiles) { + if (!container) return; + const selects = container.querySelectorAll(".task-profile-select"); selects.forEach((select) => { - const preferred = select.dataset.preferred || select.value; - select.innerHTML = ""; - if (!profiles.length) { - const option = document.createElement("option"); - option.value = ""; - option.textContent = "No profiles configured"; - select.appendChild(option); - select.disabled = true; - return; - } - - select.disabled = false; - for (const profile of profiles) { - const option = document.createElement("option"); - option.value = profile.id; - option.textContent = profile.name || "Default"; - select.appendChild(option); - } - - if (preferred && profiles.some((profile) => profile.id === preferred)) { - select.value = preferred; - } else { - select.value = profiles[0].id; - } - - select.dataset.preferred = select.value; + populateSelect(select, profiles, "No profiles configured"); }); +} + +function updateTaskProfileOptions() { + const profiles = collectProfiles().filter((profile) => isEnabled(profile.enabled)); + updateTaskProfileOptionsForContainer(tasksContainer, profiles); + + const workspaceCards = document.querySelectorAll(".workspace-card"); + workspaceCards.forEach((card) => { + const scope = getWorkspaceScopeData(card); + updateTaskProfileOptionsForContainer( + card.querySelector(".workspace-tasks"), + scope.profiles + ); + }); + + const siteCards = document.querySelectorAll(".site-card"); + siteCards.forEach((card) => { + const scope = getSiteScopeData(card); + updateTaskProfileOptionsForContainer( + card.querySelector(".site-tasks"), + scope.profiles + ); + }); + + updateShortcutOptions(); + refreshWorkspaceInheritedLists(); + refreshSiteInheritedLists(); scheduleSidebarErrors(); } -function updateEnvApiOptions() { - const apiConfigs = collectApiConfigs(); - const selects = envConfigsContainer.querySelectorAll(".env-config-api-select"); +function updateEnvApiOptionsForContainer(container, apiConfigs) { + if (!container) return; + const selects = container.querySelectorAll(".env-config-api-select"); selects.forEach((select) => { - const preferred = select.dataset.preferred || select.value; - select.innerHTML = ""; - if (!apiConfigs.length) { - const option = document.createElement("option"); - option.value = ""; - option.textContent = "No API configs configured"; - select.appendChild(option); - select.disabled = true; - return; - } - - select.disabled = false; - for (const config of apiConfigs) { - const option = document.createElement("option"); - option.value = config.id; - option.textContent = config.name || "Default"; - select.appendChild(option); - } - - if (preferred && apiConfigs.some((config) => config.id === preferred)) { - select.value = preferred; - } else { - select.value = apiConfigs[0].id; - } - 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 = getSiteApiConfigs(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 + ); + }); + }); +} + +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); + }); + }); +} + +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 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) { + envSelect.dataset.preferred = envSelect.value; + populateSelect(envSelect, envs, "No environments configured"); + } + if (profileSelect) { + profileSelect.dataset.preferred = profileSelect.value; + populateSelect(profileSelect, profiles, "No profiles configured"); + } + if (taskSelect) { + taskSelect.dataset.preferred = taskSelect.value; + populateSelect(taskSelect, tasks, "No tasks configured"); + } + }); +} + +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(".workspace-theme"); + const themeSelect = card.querySelector(".appearance-theme"); + const toolbarSelect = card.querySelector(".appearance-toolbar-position"); // Collect nested resources const envsContainer = card.querySelector(".workspace-envs"); const profilesContainer = card.querySelector(".workspace-profiles"); const tasksContainer = card.querySelector(".workspace-tasks"); - const presetsContainer = card.querySelector(".workspace-presets"); + 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. @@ -1102,42 +1448,56 @@ function collectWorkspaces() { id: card.dataset.id || newWorkspaceId(), name: (nameInput?.value || "Untitled Workspace").trim(), theme: themeSelect?.value || "inherit", + toolbarPosition: toolbarSelect?.value || "inherit", envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [], profiles: profilesContainer ? collectProfiles(profilesContainer) : [], tasks: tasksContainer ? collectTasks(tasksContainer) : [], - presets: presetsContainer ? collectPresets(presetsContainer) : [] + shortcuts: shortcutsContainer ? collectShortcuts(shortcutsContainer) : [], + disabledInherited: { + envs: collectDisabledInherited(envsInherited), + profiles: collectDisabledInherited(profilesInherited), + tasks: collectDisabledInherited(tasksInherited), + shortcuts: collectDisabledInherited(shortcutsInherited), + apiConfigs: collectDisabledInherited(apiConfigsInherited) + } }; }); } -function collectPresets(container = presetsContainer) { - const cards = [...container.querySelectorAll(".preset-card")]; +function collectShortcuts(container = shortcutsContainer) { + if (!container) return []; + const cards = [...container.querySelectorAll(".shortcut-card")]; return cards.map((card) => { - const nameInput = card.querySelector(".preset-name"); - const envSelect = card.querySelector(".preset-env"); - const profileSelect = card.querySelector(".preset-profile"); - const taskSelect = card.querySelector(".preset-task"); + 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 || newPresetId(), - name: (nameInput?.value || "Untitled Preset").trim(), + id: card.dataset.id || newShortcutId(), + name: (nameInput?.value || "Untitled Shortcut").trim(), envId: envSelect?.value || "", profileId: profileSelect?.value || "", - taskId: taskSelect?.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() + systemPrompt: (promptInput?.value || "").trim(), + enabled: enabledInput ? enabledInput.checked : true }; }); } @@ -1192,30 +1552,685 @@ function renderWorkspaceSection(title, containerClass, items, builder, newItemFa return details; } -function buildWorkspaceCard(ws) { - const card = document.createElement("div"); - card.className = "workspace-card panel"; - card.dataset.id = ws.id || newWorkspaceId(); +function buildAppearanceSection({ theme = "inherit", toolbarPosition = "inherit" } = {}) { + const details = document.createElement("details"); + details.className = "panel sub-panel"; - const header = document.createElement("div"); - header.className = "workspace-header"; - - const nameInput = document.createElement("input"); - nameInput.type = "text"; - nameInput.value = ws.name || ""; - nameInput.className = "workspace-name"; - nameInput.placeholder = "Workspace Name"; - + const summary = document.createElement("summary"); + summary.className = "panel-summary"; + summary.innerHTML = + '

Appearance

'; + 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 = "workspace-theme"; - const themes = ["inherit", "light", "dark", "system"]; + 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 = ws.theme || "inherit"; + themeSelect.value = theme || "inherit"; + themeField.appendChild(themeLabel); + themeField.appendChild(themeSelect); + + 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); + + body.appendChild(themeField); + body.appendChild(toolbarField); + details.appendChild(body); + + return details; +} + +function normalizeName(value) { + return (value || "").trim().toLowerCase(); +} + +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 buildScopeGroup(title, content) { + const wrapper = document.createElement("div"); + wrapper.className = "scope-group"; + const heading = document.createElement("div"); + heading.className = "scope-title hint-accent"; + heading.textContent = title; + wrapper.appendChild(heading); + wrapper.appendChild(content); + 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 +}) { + const details = document.createElement("details"); + details.className = "panel sub-panel"; + const summary = document.createElement("summary"); + summary.className = "panel-summary"; + summary.innerHTML = `

${title}

`; + 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 + ); + }; + + body.appendChild(buildScopeGroup("Inherited", inheritedList)); + + 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 localActions = document.createElement("div"); + localActions.className = "row"; + const spacer = document.createElement("div"); + const addBtn = document.createElement("button"); + addBtn.type = "button"; + addBtn.className = "ghost"; + addBtn.textContent = "Add"; + addBtn.addEventListener("click", () => { + const newItem = newItemFactory(localContainer); + const options = + typeof cardOptions === "function" ? cardOptions() : cardOptions; + localContainer.appendChild(buildCard(newItem, localContainer, options)); + if (module === "envs") { + updateEnvApiOptions(); + } else if (module === "profiles") { + updateTaskProfileOptions(); + } else if (module === "tasks") { + updateShortcutOptions(); + } + refreshInherited(); + scheduleSidebarErrors(); + }); + localActions.appendChild(spacer); + localActions.appendChild(addBtn); + + const localWrapper = document.createElement("div"); + localWrapper.appendChild(localActions); + 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); + 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-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 duplicateToWorkspace(module, source, workspaceId) { + 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) { + container.appendChild(card); + scheduleSidebarErrors(); + } +} + +function duplicateToSite(module, source, siteId) { + 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) { + container.appendChild(card); + scheduleSidebarErrors(); + } +} + +function buildDuplicateControls(module, getSourceData) { + const wrapper = document.createElement("div"); + wrapper.className = "dup-controls"; + + const workspaceBtn = document.createElement("button"); + workspaceBtn.type = "button"; + workspaceBtn.className = "ghost"; + workspaceBtn.textContent = "Duplicate to Workspace"; + const workspaceSelect = document.createElement("select"); + workspaceSelect.className = "dup-select hidden"; + + workspaceBtn.addEventListener("click", () => { + const targets = listWorkspaceTargets(); + fillTargetSelect(workspaceSelect, targets, "Select workspace"); + workspaceSelect.classList.toggle("hidden"); + workspaceSelect.focus(); + }); + + workspaceSelect.addEventListener("change", () => { + if (!workspaceSelect.value) return; + duplicateToWorkspace(module, getSourceData(), workspaceSelect.value); + workspaceSelect.value = ""; + workspaceSelect.classList.add("hidden"); + }); + + const siteBtn = document.createElement("button"); + siteBtn.type = "button"; + siteBtn.className = "ghost"; + siteBtn.textContent = "Duplicate to Site"; + const siteSelect = document.createElement("select"); + siteSelect.className = "dup-select hidden"; + + siteBtn.addEventListener("click", () => { + const targets = listSiteTargets(); + fillTargetSelect(siteSelect, targets, "Select site"); + siteSelect.classList.toggle("hidden"); + siteSelect.focus(); + }); + + siteSelect.addEventListener("change", () => { + if (!siteSelect.value) return; + duplicateToSite(module, getSourceData(), siteSelect.value); + siteSelect.value = ""; + siteSelect.classList.add("hidden"); + }); + + wrapper.appendChild(workspaceBtn); + wrapper.appendChild(workspaceSelect); + wrapper.appendChild(siteBtn); + wrapper.appendChild(siteSelect); + return wrapper; +} + +function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) { + const card = document.createElement("div"); + card.className = "workspace-card panel"; + card.dataset.id = ws.id || newWorkspaceId(); + + const header = document.createElement("div"); + header.className = "row workspace-header"; + + 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 deleteBtn = document.createElement("button"); deleteBtn.type = "button"; @@ -1228,68 +2243,167 @@ function buildWorkspaceCard(ws) { } }); - header.appendChild(nameInput); - header.appendChild(themeSelect); + header.appendChild(nameField); header.appendChild(deleteBtn); card.appendChild(header); - // Subsections - const envSection = renderWorkspaceSection( - "Environments", - "workspace-envs", - ws.envConfigs, - buildEnvConfigCard, - (container) => ({ + const appearanceSection = buildAppearanceSection({ + theme: ws.theme || "inherit", + toolbarPosition: ws.toolbarPosition || "inherit" + }); + card.appendChild(appearanceSection); + + const disabledInherited = ws.disabledInherited || {}; + const globalApiConfigs = collectApiConfigs(); + const apiConfigSection = document.createElement("details"); + apiConfigSection.className = "panel sub-panel"; + const apiSummary = document.createElement("summary"); + apiSummary.className = "panel-summary"; + apiSummary.innerHTML = + '

API Configurations

'; + 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); + card.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, + newItemFactory: (container) => ({ id: newEnvConfigId(), name: buildUniqueDefaultName(collectNames(container, ".env-config-name")), - apiConfigId: collectApiConfigs()[0]?.id || "", - systemPrompt: DEFAULT_SYSTEM_PROMPT + apiConfigId: getWorkspaceApiConfigs(card)[0]?.id || "", + systemPrompt: DEFAULT_SYSTEM_PROMPT, + enabled: true }) - ); - card.appendChild(envSection); + }); + card.appendChild(envSection.details); - const profileSection = renderWorkspaceSection( - "Profiles", - "workspace-profiles", - ws.profiles, - buildProfileCard, - (container) => ({ + const profileSection = buildScopedModuleSection({ + title: "Profiles", + module: "profiles", + parentItems: () => collectProfiles(), + localItems: ws.profiles || [], + disabledNames: disabledInherited.profiles, + localLabel: "Workspace-specific", + localContainerClass: "workspace-profiles", + buildCard: buildProfileCard, + newItemFactory: (container) => ({ id: newProfileId(), name: buildUniqueDefaultName(collectNames(container, ".profile-name")), - text: "" - }) - ); - card.appendChild(profileSection); - - const taskSection = renderWorkspaceSection( - "Tasks", - "workspace-tasks", - ws.tasks, - buildTaskCard, - (container) => ({ - id: newTaskId(), - name: buildUniqueDefaultName(collectNames(container, ".task-name")), text: "", - defaultEnvId: "", - defaultProfileId: "" + enabled: true }) - ); - card.appendChild(taskSection); + }); + card.appendChild(profileSection.details); - const presetSection = renderWorkspaceSection( - "Presets", - "workspace-presets", - ws.presets, - buildPresetCard, - (container) => ({ - id: newPresetId(), - name: "New Preset", - envId: "", - profileId: "", - taskId: "" - }) + const taskSection = buildScopedModuleSection({ + title: "Tasks", + module: "tasks", + parentItems: () => collectTasks(), + localItems: ws.tasks || [], + disabledNames: disabledInherited.tasks, + localLabel: "Workspace-specific", + localContainerClass: "workspace-tasks", + buildCard: buildTaskCard, + cardOptions: () => { + const scope = getWorkspaceScopeData(card); + return { envs: scope.envs, profiles: scope.profiles }; + }, + newItemFactory: (container) => { + const scope = getWorkspaceScopeData(card); + return { + id: newTaskId(), + name: buildUniqueDefaultName(collectNames(container, ".task-name")), + text: "", + defaultEnvId: scope.envs[0]?.id || "", + defaultProfileId: scope.profiles[0]?.id || "", + enabled: true + }; + } + }); + card.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, + 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: "New Shortcut", + envId: scope.envs[0]?.id || "", + profileId: scope.profiles[0]?.id || "", + taskId: scope.tasks[0]?.id || "", + enabled: true + }; + } + }); + card.appendChild(shortcutSection.details); + + const sitesSection = document.createElement("details"); + sitesSection.className = "panel sub-panel"; + const sitesSummary = document.createElement("summary"); + sitesSummary.className = "panel-summary"; + sitesSummary.innerHTML = + '

Sites

'; + sitesSection.appendChild(sitesSummary); + const sitesBody = document.createElement("div"); + sitesBody.className = "panel-body"; + const siteList = document.createElement("div"); + siteList.className = "sites-list"; + const ownedSites = (allSites || []).filter( + (site) => (site.workspaceId || "global") === card.dataset.id ); - card.appendChild(presetSection); + if (!ownedSites.length) { + const empty = document.createElement("div"); + empty.className = "hint"; + empty.textContent = "No sites inherit from this workspace."; + siteList.appendChild(empty); + } else { + for (const site of ownedSites) { + const link = document.createElement("a"); + link.href = "#"; + link.textContent = 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")); + } + }); + siteList.appendChild(link); + } + } + sitesBody.appendChild(siteList); + sitesSection.appendChild(sitesBody); + card.appendChild(sitesSection); return card; } @@ -1299,15 +2413,39 @@ function collectSites() { return cards.map((card) => { const patternInput = card.querySelector(".site-pattern"); const workspaceSelect = card.querySelector(".site-workspace"); + const themeSelect = card.querySelector(".appearance-theme"); + const toolbarSelect = card.querySelector(".appearance-toolbar-position"); + 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(), urlPattern: (patternInput?.value || "").trim(), - workspaceId: workspaceSelect?.value || "global" + workspaceId: workspaceSelect?.value || "global", + theme: themeSelect?.value || "inherit", + toolbarPosition: toolbarSelect?.value || "inherit", + 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) { +function buildSiteCard(site, allWorkspaces = []) { const card = document.createElement("div"); card.className = "site-card panel"; card.dataset.id = site.id || newSiteId(); @@ -1326,6 +2464,10 @@ function buildSiteCard(site) { 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); @@ -1335,25 +2477,39 @@ function buildSiteCard(site) { wsLabel.textContent = "Workspace"; const wsSelect = document.createElement("select"); wsSelect.className = "site-workspace"; - - // Populate workspaces - const workspaces = collectWorkspaces(); const globalOpt = document.createElement("option"); globalOpt.value = "global"; globalOpt.textContent = "Global"; wsSelect.appendChild(globalOpt); - - for (const ws of workspaces) { + for (const ws of allWorkspaces) { const opt = document.createElement("option"); opt.value = ws.id; - opt.textContent = ws.name; + opt.textContent = ws.name || "Untitled Workspace"; wsSelect.appendChild(opt); } wsSelect.value = site.workspaceId || "global"; - wsField.appendChild(wsLabel); wsField.appendChild(wsSelect); + 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"; @@ -1368,14 +2524,169 @@ function buildSiteCard(site) { row.appendChild(deleteBtn); card.appendChild(row); + const appearanceSection = buildAppearanceSection({ + theme: site.theme || "inherit", + toolbarPosition: site.toolbarPosition || "inherit" + }); + card.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"; + const apiSummary = document.createElement("summary"); + apiSummary.className = "panel-summary"; + apiSummary.innerHTML = + '

API Configurations

'; + 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); + card.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, + newItemFactory: (container) => ({ + id: newEnvConfigId(), + name: buildUniqueDefaultName(collectNames(container, ".env-config-name")), + apiConfigId: getSiteApiConfigs(card)[0]?.id || "", + systemPrompt: DEFAULT_SYSTEM_PROMPT, + enabled: true + }) + }); + card.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, + newItemFactory: (container) => ({ + id: newProfileId(), + name: buildUniqueDefaultName(collectNames(container, ".profile-name")), + text: "", + enabled: true + }) + }); + card.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, + cardOptions: () => { + const scope = getSiteScopeData(card); + return { envs: scope.envs, profiles: scope.profiles }; + }, + newItemFactory: (container) => { + const scope = getSiteScopeData(card); + return { + id: newTaskId(), + name: buildUniqueDefaultName(collectNames(container, ".task-name")), + text: "", + defaultEnvId: scope.envs[0]?.id || "", + defaultProfileId: scope.profiles[0]?.id || "", + enabled: true + }; + } + }); + card.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, + 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: "New Shortcut", + envId: scope.envs[0]?.id || "", + profileId: scope.profiles[0]?.id || "", + taskId: scope.tasks[0]?.id || "", + enabled: true + }; + } + }); + card.appendChild(shortcutSection.details); + return card; } -function buildTaskCard(task, container = tasksContainer) { +function buildTaskCard(task, container = tasksContainer, options = {}) { const card = document.createElement("div"); card.className = "task-card"; card.dataset.id = task.id || newTaskId(); + const enabledLabel = document.createElement("label"); + enabledLabel.className = "toggle-label"; + const enabledInput = document.createElement("input"); + enabledInput.type = "checkbox"; + enabledInput.className = "config-enabled"; + enabledInput.checked = task.enabled !== false; + enabledInput.addEventListener("change", () => { + updateShortcutOptions(); + scheduleSidebarErrors(); + }); + enabledLabel.appendChild(enabledInput); + enabledLabel.appendChild(document.createTextNode("Enabled")); + const nameField = document.createElement("div"); nameField.className = "field"; const nameLabel = document.createElement("label"); @@ -1418,6 +2729,15 @@ function buildTaskCard(task, container = tasksContainer) { 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"); @@ -1436,10 +2756,6 @@ function buildTaskCard(task, container = tasksContainer) { addBelowBtn.type = "button"; addBelowBtn.className = "ghost add-below"; addBelowBtn.textContent = "Add"; - const duplicateBtn = document.createElement("button"); - duplicateBtn.type = "button"; - duplicateBtn.className = "ghost duplicate"; - duplicateBtn.textContent = "Duplicate"; const deleteBtn = document.createElement("button"); deleteBtn.type = "button"; deleteBtn.className = "ghost delete"; @@ -1470,79 +2786,74 @@ function buildTaskCard(task, container = tasksContainer) { const name = buildUniqueDefaultName( 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: getTopEnvId(), - defaultProfileId: getTopProfileId() - }, container); + defaultEnvId, + defaultProfileId + }, container, scope); card.insertAdjacentElement("afterend", newCard); updateTaskControls(container); updateTaskEnvOptions(); updateTaskProfileOptions(); }); - duplicateBtn.addEventListener("click", () => { - const copy = { - id: newTaskId(), - name: ensureUniqueName( - `${nameInput.value || "Untitled"} Copy`, - collectNames(container, ".task-name") - ), - text: textArea.value, - defaultEnvId: envSelect.value || "", - defaultProfileId: profileSelect.value || "" - }; - const newCard = buildTaskCard(copy, container); - card.insertAdjacentElement("afterend", newCard); - updateTaskControls(container); - updateTaskEnvOptions(); - updateTaskProfileOptions(); - }); + const duplicateControls = buildDuplicateControls("tasks", () => ({ + id: card.dataset.id, + name: nameInput.value || "Untitled", + text: textArea.value, + defaultEnvId: envSelect.value || "", + defaultProfileId: profileSelect.value || "", + enabled: enabledInput.checked + })); deleteBtn.addEventListener("click", () => { card.remove(); updateTaskControls(container); + updateShortcutOptions(); }); actions.appendChild(moveTopBtn); actions.appendChild(moveUpBtn); actions.appendChild(moveDownBtn); actions.appendChild(addBelowBtn); - actions.appendChild(duplicateBtn); + actions.appendChild(duplicateControls); actions.appendChild(deleteBtn); + card.appendChild(enabledLabel); card.appendChild(nameField); card.appendChild(envField); card.appendChild(profileField); card.appendChild(textField); card.appendChild(actions); + nameInput.addEventListener("input", () => updateShortcutOptions()); + return card; } -function collectPresets() { - const cards = [...presetsContainer.querySelectorAll(".preset-card")]; - return cards.map((card) => { - const nameInput = card.querySelector(".preset-name"); - const envSelect = card.querySelector(".preset-env"); - const profileSelect = card.querySelector(".preset-profile"); - const taskSelect = card.querySelector(".preset-task"); - return { - id: card.dataset.id || newPresetId(), - name: (nameInput?.value || "Untitled Preset").trim(), - envId: envSelect?.value || "", - profileId: profileSelect?.value || "", - taskId: taskSelect?.value || "" - }; - }); -} - -function buildPresetCard(preset) { +function buildShortcutCard(shortcut, _container, options = {}) { const card = document.createElement("div"); - card.className = "preset-card"; - card.dataset.id = preset.id || newPresetId(); + card.className = "shortcut-card"; + card.dataset.id = shortcut.id || newShortcutId(); + + const enabledLabel = document.createElement("label"); + enabledLabel.className = "toggle-label"; + const enabledInput = document.createElement("input"); + enabledInput.type = "checkbox"; + enabledInput.className = "config-enabled"; + enabledInput.checked = shortcut.enabled !== false; + enabledInput.addEventListener("change", () => { + scheduleSidebarErrors(); + }); + enabledLabel.appendChild(enabledInput); + enabledLabel.appendChild(document.createTextNode("Enabled")); const nameField = document.createElement("div"); nameField.className = "field"; @@ -1550,8 +2861,9 @@ function buildPresetCard(preset) { nameLabel.textContent = "Name"; const nameInput = document.createElement("input"); nameInput.type = "text"; - nameInput.value = preset.name || ""; - nameInput.className = "preset-name"; + nameInput.value = shortcut.name || ""; + nameInput.className = "shortcut-name"; + nameInput.addEventListener("input", () => scheduleSidebarErrors()); nameField.appendChild(nameLabel); nameField.appendChild(nameInput); @@ -1560,15 +2872,17 @@ function buildPresetCard(preset) { const envLabel = document.createElement("label"); envLabel.textContent = "Environment"; const envSelect = document.createElement("select"); - envSelect.className = "preset-env"; - const envs = collectEnvConfigs(); // Global only for now + envSelect.className = "shortcut-env"; + 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 = preset.envId || (envs[0]?.id || ""); + envSelect.value = shortcut.envId || (envs[0]?.id || ""); envField.appendChild(envLabel); envField.appendChild(envSelect); @@ -1577,15 +2891,17 @@ function buildPresetCard(preset) { const profileLabel = document.createElement("label"); profileLabel.textContent = "Profile"; const profileSelect = document.createElement("select"); - profileSelect.className = "preset-profile"; - const profiles = collectProfiles(); // Global only + profileSelect.className = "shortcut-profile"; + 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 = preset.profileId || (profiles[0]?.id || ""); + profileSelect.value = shortcut.profileId || (profiles[0]?.id || ""); profileField.appendChild(profileLabel); profileField.appendChild(profileSelect); @@ -1594,15 +2910,17 @@ function buildPresetCard(preset) { const taskLabel = document.createElement("label"); taskLabel.textContent = "Task"; const taskSelect = document.createElement("select"); - taskSelect.className = "preset-task"; - const tasks = collectTasks(); // Global only + taskSelect.className = "shortcut-task"; + 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 = preset.taskId || (tasks[0]?.id || ""); + taskSelect.value = shortcut.taskId || (tasks[0]?.id || ""); taskField.appendChild(taskLabel); taskField.appendChild(taskSelect); @@ -1616,10 +2934,21 @@ function buildPresetCard(preset) { scheduleSidebarErrors(); }); + card.appendChild(enabledLabel); card.appendChild(nameField); card.appendChild(envField); card.appendChild(profileField); card.appendChild(taskField); + card.appendChild( + buildDuplicateControls("shortcuts", () => ({ + id: card.dataset.id, + name: nameInput.value || "Untitled Shortcut", + envId: envSelect.value || "", + profileId: profileSelect.value || "", + taskId: taskSelect.value || "", + enabled: enabledInput.checked + })) + ); card.appendChild(deleteBtn); return card; @@ -1639,18 +2968,21 @@ function updateTaskControls(container = tasksContainer) { } 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 || "" + defaultProfileId: profileSelect?.value || "", + enabled: enabledInput ? enabledInput.checked : true }; }); } @@ -1664,6 +2996,11 @@ function updateSidebarErrors() { 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; @@ -1672,6 +3009,13 @@ function updateSidebarErrors() { 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; @@ -1690,31 +3034,86 @@ function updateSidebarErrors() { } }; - checkNameInputs(tasksContainer, ".task-name", "Task presets"); + 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"); - if (!tasks.length) errors.push("No task presets configured."); - if (!envs.length) errors.push("No environments configured."); - if (!profiles.length) errors.push("No profiles configured."); - if (!apiConfigs.length) errors.push("No API configs configured."); - if (!apiKeys.length) errors.push("No API keys configured."); + 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` + ); + }); - if (tasks.length) { - const defaultTask = tasks[0]; + const siteCards = [...sitesContainer.querySelectorAll(".site-card")]; + siteCards.forEach((card) => { + const label = 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` + ); + }); + + 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."); + + if (enabledTasks.length) { + const defaultTask = enabledTasks[0]; if (!defaultTask.text) errors.push("Default task prompt is empty."); const defaultEnv = - envs.find((env) => env.id === defaultTask.defaultEnvId) || envs[0]; + enabledEnvs.find((env) => env.id === defaultTask.defaultEnvId) || + enabledEnvs[0]; if (!defaultEnv) { errors.push("Default task environment is missing."); } const defaultProfile = - profiles.find((profile) => profile.id === defaultTask.defaultProfileId) || - profiles[0]; + enabledProfiles.find((profile) => profile.id === defaultTask.defaultProfileId) || + enabledProfiles[0]; if (!defaultProfile) { errors.push("Default task profile is missing."); } else if (!defaultProfile.text) { @@ -1722,7 +3121,7 @@ function updateSidebarErrors() { } const defaultApiConfig = defaultEnv - ? apiConfigs.find((config) => config.id === defaultEnv.apiConfigId) + ? enabledApiConfigs.find((config) => config.id === defaultEnv.apiConfigId) : null; if (!defaultApiConfig) { errors.push("Default environment is missing an API config."); @@ -1748,25 +3147,44 @@ function updateSidebarErrors() { defaultApiConfig?.requestTemplate?.includes("API_KEY_GOES_HERE") ); if (needsKey) { - const key = apiKeys.find((entry) => entry.id === defaultApiConfig?.apiKeyId); + const key = enabledApiKeys.find( + (entry) => entry.id === defaultApiConfig?.apiKeyId + ); if (!key || !key.key) { errors.push("Default API config is missing an API key."); } } } + 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; + } + } + } + if (!errors.length) { sidebarErrorsEl.classList.add("hidden"); sidebarErrorsEl.textContent = ""; + renderGlobalSitesList(collectSites()); return; } sidebarErrorsEl.textContent = errors.map((error) => `- ${error}`).join("\n"); sidebarErrorsEl.classList.remove("hidden"); + renderGlobalSitesList(collectSites()); } async function loadSettings() { - const { + let { apiKey = "", apiKeys = [], activeApiKeyId = "", @@ -1782,11 +3200,13 @@ async function loadSettings() { systemPrompt = "", resume = "", tasks = [], - presets = [], + shortcuts = [], + presets: legacyPresets = [], theme = "system", workspaces = [], sites = [], - toolbarPosition = "bottom-right" + toolbarPosition = "bottom-right", + toolbarAutoHide: storedToolbarAutoHide = true } = await getStorage([ "apiKey", "apiKeys", @@ -1803,11 +3223,13 @@ async function loadSettings() { "systemPrompt", "resume", "tasks", + "shortcuts", "presets", "theme", "workspaces", "sites", - "toolbarPosition" + "toolbarPosition", + "toolbarAutoHide" ]); themeSelect.value = theme; @@ -1816,8 +3238,68 @@ async function loadSettings() { if (toolbarPositionSelect) { toolbarPositionSelect.value = toolbarPosition; } + if (toolbarAutoHide) { + toolbarAutoHide.checked = Boolean(storedToolbarAutoHide); + } - // Load basic resources first so they are available for presets/workspaces + 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", + envConfigs: normalizeConfigList(workspace.envConfigs), + profiles: normalizeConfigList(workspace.profiles), + tasks: normalizeConfigList(workspace.tasks), + shortcuts: normalizeConfigList(workspace.shortcuts), + disabledInherited: normalizeDisabledInherited(workspace.disabledInherited) + }; + }); + } + + if (Array.isArray(sites)) { + sites = sites.map((site) => { + if (!site || typeof site !== "object") return site; + return { + ...site, + workspaceId: site.workspaceId || "global", + theme: site.theme || "inherit", + toolbarPosition: site.toolbarPosition || "inherit", + envConfigs: normalizeConfigList(site.envConfigs), + profiles: normalizeConfigList(site.profiles), + tasks: normalizeConfigList(site.tasks), + shortcuts: normalizeConfigList(site.shortcuts), + disabledInherited: normalizeDisabledInherited(site.disabledInherited) + }; + }); + } + + // Load basic resources first so they are available for shortcuts/workspaces envConfigsContainer.innerHTML = ""; // ... (existing logic handles this later) @@ -1825,25 +3307,20 @@ async function loadSettings() { // loadSettings currently renders cards later in the function. // I need to ensure render order. - // Actually, loadSettings renders cards in order. I should just add presets rendering at the end. + // Actually, loadSettings renders cards in order. I should just add shortcuts rendering at the end. - workspacesContainer.innerHTML = ""; - for (const ws of workspaces) { - workspacesContainer.appendChild(buildWorkspaceCard(ws)); - } - - sitesContainer.innerHTML = ""; - for (const site of sites) { - sitesContainer.appendChild(buildSiteCard(site)); - } - - // I'll render presets after tasks are rendered. + // 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 }; + const migrated = { + id: newApiKeyId(), + name: "Default", + key: apiKey, + enabled: true + }; resolvedKeys = [migrated]; resolvedActiveId = migrated.id; await chrome.storage.local.set({ @@ -1851,6 +3328,16 @@ async function loadSettings() { 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; @@ -1884,7 +3371,8 @@ async function loadSettings() { apiKeyId: resolvedActiveId || resolvedKeys[0]?.id || "", apiUrl: "", requestTemplate: "", - advanced: false + advanced: false, + enabled: true }; resolvedConfigs = [migrated]; resolvedActiveConfigId = migrated.id; @@ -1899,9 +3387,16 @@ async function loadSettings() { apiKeyId: config.apiKeyId || fallbackKeyId, apiUrl: config.apiUrl || "", requestTemplate: config.requestTemplate || "", - advanced: Boolean(config.advanced) + advanced: Boolean(config.advanced), + enabled: config.enabled !== false })); - if (withKeys.some((config, index) => config.apiKeyId !== resolvedConfigs[index].apiKeyId)) { + if ( + withKeys.some( + (config, index) => + config.apiKeyId !== resolvedConfigs[index].apiKeyId || + config.enabled !== resolvedConfigs[index].enabled + ) + ) { resolvedConfigs = withKeys; await chrome.storage.local.set({ apiConfigs: resolvedConfigs }); } @@ -1930,7 +3425,8 @@ async function loadSettings() { id: newEnvConfigId(), name: "Default", apiConfigId: fallbackApiConfigId, - systemPrompt: systemPrompt || DEFAULT_SYSTEM_PROMPT + systemPrompt: systemPrompt || DEFAULT_SYSTEM_PROMPT, + enabled: true }; resolvedEnvConfigs = [migrated]; await chrome.storage.local.set({ @@ -1941,13 +3437,15 @@ async function loadSettings() { const withDefaults = resolvedEnvConfigs.map((config) => ({ ...config, apiConfigId: config.apiConfigId || fallbackApiConfigId, - systemPrompt: config.systemPrompt ?? "" + 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.systemPrompt || "") !== (original.systemPrompt || "") || + config.enabled !== original.enabled ); }); if (needsUpdate) { @@ -1977,7 +3475,8 @@ async function loadSettings() { id: newProfileId(), name: "Default", text: resume || "", - type: "Resume" + type: "Resume", + enabled: true }; resolvedProfiles = [migrated]; await chrome.storage.local.set({ profiles: resolvedProfiles }); @@ -1985,12 +3484,14 @@ async function loadSettings() { const normalized = resolvedProfiles.map((profile) => ({ ...profile, text: profile.text ?? "", - type: profile.type === "Profile" ? "Profile" : "Resume" + 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.type || "Resume") !== (resolvedProfiles[index]?.type || "Resume") || + profile.enabled !== resolvedProfiles[index]?.enabled ); if (needsUpdate) { resolvedProfiles = normalized; @@ -2011,7 +3512,8 @@ async function loadSettings() { ? tasks.map((task) => ({ ...task, defaultEnvId: task.defaultEnvId || defaultEnvId, - defaultProfileId: task.defaultProfileId || defaultProfileId + defaultProfileId: task.defaultProfileId || defaultProfileId, + enabled: task.enabled !== false })) : []; if ( @@ -2019,7 +3521,8 @@ async function loadSettings() { normalizedTasks.some( (task, index) => task.defaultEnvId !== tasks[index]?.defaultEnvId || - task.defaultProfileId !== tasks[index]?.defaultProfileId + task.defaultProfileId !== tasks[index]?.defaultProfileId || + task.enabled !== tasks[index]?.enabled ) ) { await chrome.storage.local.set({ tasks: normalizedTasks }); @@ -2048,50 +3551,91 @@ async function loadSettings() { updateTaskEnvOptions(); updateTaskProfileOptions(); - presetsContainer.innerHTML = ""; - for (const preset of presets) { - presetsContainer.appendChild(buildPresetCard(preset)); + 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)); + } + + workspacesContainer.innerHTML = ""; + for (const ws of workspaces) { + workspacesContainer.appendChild(buildWorkspaceCard(ws, workspaces)); + } + + sitesContainer.innerHTML = ""; + for (const site of sites) { + sitesContainer.appendChild(buildSiteCard(site, workspaces)); + } + + updateEnvApiOptions(); + refreshWorkspaceInheritedLists(); + refreshSiteInheritedLists(); updateSidebarErrors(); updateToc(workspaces, sites); + renderGlobalSitesList(sites); } async function saveSettings() { - const tasks = collectTasks(); - const presets = collectPresets(); - const apiKeys = collectApiKeys(); - const apiConfigs = collectApiConfigs(); - const envConfigs = collectEnvConfigs(); - const profiles = collectProfiles(); - const workspaces = collectWorkspaces(); - const sites = collectSites(); - 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, - presets, - theme: themeSelect.value, - toolbarPosition: toolbarPositionSelect ? toolbarPositionSelect.value : "bottom-right", - workspaces, - sites - }); - setStatus("Saved."); + 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 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", + toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true, + workspaces, + sites + }); + await chrome.storage.local.remove("presets"); + setStatus("Saved."); + } catch (error) { + console.error("Save failed:", error); + setStatus("Save failed. Check console."); + } } saveBtn.addEventListener("click", () => void saveSettings()); @@ -2165,7 +3709,8 @@ addEnvConfigBtn.addEventListener("click", () => { const name = buildUniqueDefaultName( collectNames(envConfigsContainer, ".env-config-name") ); - const fallbackApiConfigId = collectApiConfigs()[0]?.id || ""; + const fallbackApiConfigId = + getApiConfigsForEnvContainer(envConfigsContainer)[0]?.id || ""; const newCard = buildEnvConfigCard({ id: newEnvConfigId(), name, @@ -2206,9 +3751,16 @@ addWorkspaceBtn.addEventListener("click", () => { const newCard = buildWorkspaceCard({ id: newWorkspaceId(), name: "New Workspace", - theme: "inherit" - }); + theme: "inherit", + toolbarPosition: "inherit", + envConfigs: [], + profiles: [], + tasks: [], + shortcuts: [], + disabledInherited: normalizeDisabledInherited() + }, collectWorkspaces(), collectSites()); workspacesContainer.appendChild(newCard); + refreshWorkspaceInheritedLists(); scheduleSidebarErrors(); updateToc(collectWorkspaces(), collectSites()); }); @@ -2217,30 +3769,55 @@ addSiteBtn.addEventListener("click", () => { const newCard = buildSiteCard({ id: newSiteId(), urlPattern: "", - workspaceId: "global" - }); + workspaceId: "global", + theme: "inherit", + toolbarPosition: "inherit", + envConfigs: [], + profiles: [], + tasks: [], + shortcuts: [], + disabledInherited: normalizeDisabledInherited() + }, collectWorkspaces()); sitesContainer.appendChild(newCard); + refreshSiteInheritedLists(); scheduleSidebarErrors(); updateToc(collectWorkspaces(), collectSites()); }); -addPresetBtn.addEventListener("click", () => { - const newCard = buildPresetCard({ - id: newPresetId(), - name: "New Preset", +addShortcutBtn.addEventListener("click", () => { + const newCard = buildShortcutCard({ + id: newShortcutId(), + name: "New Shortcut", envId: "", profileId: "", taskId: "" }); - presetsContainer.appendChild(newCard); + shortcutsContainer.appendChild(newCard); scheduleSidebarErrors(); }); -themeSelect.addEventListener("change", () => applyTheme(themeSelect.value)); themeSelect.addEventListener("change", () => applyTheme(themeSelect.value)); loadSettings(); +function openDetailsChain(target) { + let node = target; + while (node) { + if (node.tagName === "DETAILS") { + node.open = true; + } + node = node.parentElement?.closest("details"); + } +} + +function toggleTocSection(item, sub, force) { + if (!sub) return; + const shouldExpand = + typeof force === "boolean" ? force : !sub.classList.contains("expanded"); + sub.classList.toggle("expanded", shouldExpand); + item.classList.toggle("expanded", shouldExpand); +} + function updateToc(workspaces, sites) { const wsList = document.getElementById("toc-workspaces-list"); if (!wsList) return; @@ -2264,9 +3841,17 @@ function updateToc(workspaces, sites) { itemDiv.appendChild(a); const subUl = document.createElement("ul"); - subUl.className = "toc-sub hidden"; + subUl.className = "toc-sub"; - const sections = ["Environments", "Profiles", "Tasks", "Presets"]; + const sections = [ + "Appearance", + "API Configurations", + "Environments", + "Profiles", + "Tasks", + "Toolbar Shortcuts", + "Sites" + ]; for (const section of sections) { const subLi = document.createElement("li"); const subA = document.createElement("a"); @@ -2282,12 +3867,11 @@ function updateToc(workspaces, sites) { d.querySelector(".panel-summary").textContent.includes(section) ); if (details) { - details.open = true; + openDetailsChain(details); details.scrollIntoView({ behavior: "smooth", block: "start" }); - document.getElementById("workspaces-panel").open = true; } else { card.scrollIntoView({ behavior: "smooth", block: "start" }); - document.getElementById("workspaces-panel").open = true; + openDetailsChain(document.getElementById("workspaces-panel")); } } }); @@ -2296,13 +3880,10 @@ function updateToc(workspaces, sites) { } itemDiv.addEventListener("click", (e) => { - // Toggle if not clicking the link directly - if (!e.target.closest("a")) { - e.preventDefault(); - e.stopPropagation(); - subUl.classList.toggle("expanded"); - itemDiv.classList.toggle("expanded"); - } + if (e.target.closest("a")) return; + e.preventDefault(); + e.stopPropagation(); + toggleTocSection(itemDiv, subUl); }); a.addEventListener("click", (e) => { @@ -2311,12 +3892,9 @@ function updateToc(workspaces, sites) { const card = document.querySelector(`.workspace-card[data-id="${ws.id}"]`); if (card) { card.scrollIntoView({ behavior: "smooth", block: "start" }); - document.getElementById("workspaces-panel").open = true; - - // Also expand sub-list - subUl.classList.add("expanded"); - itemDiv.classList.add("expanded"); + openDetailsChain(document.getElementById("workspaces-panel")); } + toggleTocSection(itemDiv, subUl); }); li.appendChild(itemDiv); @@ -2338,7 +3916,7 @@ function updateToc(workspaces, sites) { const card = document.querySelector(`.site-card[data-id="${site.id}"]`); if (card) { card.scrollIntoView({ behavior: "smooth", block: "center" }); - document.getElementById("sites-panel").open = true; + openDetailsChain(document.getElementById("sites-panel")); } }); li.appendChild(a); @@ -2348,7 +3926,7 @@ function updateToc(workspaces, sites) { } function initToc() { - const items = document.querySelectorAll(".toc-item"); + const items = document.querySelectorAll(".toc-links > ul > li > .toc-item"); items.forEach(item => { item.addEventListener("click", (e) => { const sub = item.nextElementSibling; @@ -2358,17 +3936,9 @@ function initToc() { const link = e.target.closest("a"); const href = link.getAttribute("href"); if (href && href.startsWith("#")) { - // Let default behavior happen? No, prevent default if we want smooth scroll/open - // But here we rely on anchor. - // Just expand TOC. + openDetailsChain(document.querySelector(href)); if (sub && sub.classList.contains("toc-sub")) { - sub.classList.add("expanded"); - item.classList.add("expanded"); - } - // Open details - const target = document.querySelector(href); - if (target && target.tagName === "DETAILS") { - target.open = true; + toggleTocSection(item, sub); } } return; @@ -2376,13 +3946,32 @@ function initToc() { // Toggle sub-list on row click (excluding link) if (sub && sub.classList.contains("toc-sub")) { - e.preventDefault(); - e.stopPropagation(); - sub.classList.toggle("expanded"); - item.classList.toggle("expanded"); + e.preventDefault(); + e.stopPropagation(); + toggleTocSection(item, sub); } }); }); + + const subLinks = document.querySelectorAll(".toc-sub a[href^=\"#\"]"); + subLinks.forEach((link) => { + const href = link.getAttribute("href"); + if (!href || href === "#") return; + link.addEventListener("click", (e) => { + const target = document.querySelector(href); + if (target) { + openDetailsChain(target); + target.scrollIntoView({ behavior: "smooth", block: "start" }); + } + const sub = link.closest(".toc-sub"); + const parentItem = sub?.previousElementSibling; + if (parentItem?.classList?.contains("toc-item")) { + toggleTocSection(parentItem, sub, true); + } + e.preventDefault(); + e.stopPropagation(); + }); + }); } document.addEventListener("DOMContentLoaded", initToc);