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 @@
-
+
@@ -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);