From f0db7bb74a35170e2394b3aac4fa13344e8bc912 Mon Sep 17 00:00:00 2001 From: Peisong Xiao Date: Tue, 20 Jan 2026 05:41:07 +0000 Subject: [PATCH] v0.4.8-dev New Release (#3) # New Features - Added custom prompt mode - Always use the default environment and profile for a more compact UI - Added option to hide the toolbar when it's empty - Added documentation and icon # Fixed bugs - Fixed issue with config returning to defaults - Fixed TOC lag when cards update - Fixed some UI consistency issues - Dynamically show site text char count in popup UI Reviewed-on: https://git.peisongxiao.com/peisongxiao/SiteCompanion/pulls/3 --- README.md | 239 +++++++++++++++++++++ sitecompanion/background.js | 2 + sitecompanion/content.js | 70 ++++++- sitecompanion/icon128.png | Bin 0 -> 14495 bytes sitecompanion/manifest.json | 5 +- sitecompanion/popup.css | 99 ++++++++- sitecompanion/popup.html | 54 ++++- sitecompanion/popup.js | 384 +++++++++++++++++++++++++++++++--- sitecompanion/settings.css | 4 + sitecompanion/settings.html | 18 +- sitecompanion/settings.js | 400 +++++++++++++++++++++++++++++++++++- 11 files changed, 1203 insertions(+), 72 deletions(-) create mode 100644 README.md create mode 100644 sitecompanion/icon128.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6e7a29 --- /dev/null +++ b/README.md @@ -0,0 +1,239 @@ +# SiteCompanion +SiteCompanion is a browser extension that allows you to run AI +pipelines on the visible text of any site. + +> SiteCompanion saves you from copying and pasting to run AI on sites. + +## Setup +1. Download the code (will be published to the Chrome Store soon) and + load the extension code (`sitecompanion/`) as an unpacked + extension. +2. Configure your API keys, API endpoints, environments, profiles, and + tasks. +3. Go to a site. +4. Choose to either paste in part of the site for structured matching + or use the full text of the site. +5. Start running your tasks! + +## Execution +How `SiteCompanion` works. + +See [Configurations and Terms](#configurations-and-terms) for details +on specific terms. + +### Model +The execution of `SiteCompanion` strictly follows this model: + +``` +Extract Site Text -> Build Prompt -> Run (Send Request) -> Receive Output -> Stop +``` + +### Prompt Building +The prompt will always be built in the following order: + +``` +System prompt -> Profile -> Task prompt -> Extracted Site Text +``` + +### Extension Popup +This is the control panel for `SiteCompanion` on sites. + +The following sections describe the modes of the extension popup. + +#### Configuration - Pasting Mode +This is the mode the popup UI is in when on an unknown site. + +You can choose to try extracting the minimal enclosing class that +contains the pasted text or the full text of the body of the current +site. Choosing either will proceed to [Confirmation +Mode](#configuration-confirmation-mode). + +#### Configuration - Confirmation Mode +This is the mode to review the extracted text based on your current +site's contents and confirm the configuration of the current site +including the name, URL match pattern and the workspace it belongs to. + +Confirming will add the current site to the list of known sites and +enter [Normal Mode](#normal-mode). + +See [Site-Specific Configurations](#site-specific-configurations) for +configuration details. + +#### Normal Mode +This is the mode where most of the actions happen. + +The knobs for running a task: +- Environment: the environment in which the task is running. +- Profile: the user profile used by the task. +- Task: the task to run. +- "Custom" button: enters [Custom Mode](#custom-mode). +- "Run" button: run the task with the current environment and profile. + +And a few buttons for the output buffer: +- "Copy" button: copies the contents of the output buffer in text. +- "Copy Markdown" button: copies the contents of the output buffer + with Markdown formatting. +- "Clear" button: clears the contents of the output buffer. + +#### Custom Mode +This is a variant of [Normal Mode](#normal-mode) to allow editing of a +custom, task prompt that's not saved to the configuration of the +extension. + +The "Normal" button will go back to [Normal Mode](#normal-mode). + +*The environment and profile selectors will always be active when in +this mode.* + +### Toolbar +Place predefined shortcuts for environment + profile + task +combinations in a floating toolbar. + +*Note that clicking on the toolbar will open the popup and run the +shortcut.* + +## Configurations and Terms +The full list of configuration options and details about the terms we +use. + +### Scope and Workspaces +Every operable site for `SiteCompanion` is in a workspace. + +There are three scopes: +1. Global workspace ("global" for short): defines the baseline + configurations. +2. User-defined workspace ("workspace" for short): defines the + configurations for a class of workflows. Workspaces always inherit + the global configurations. +3. Site-specific ("site" for short): defines the configuration for each + site. May inherit the global workspace or a user-defined + workspace. Must always inherit a workspace. + +`SiteCompanion` operates on a specific site at a time. + +### Configuration Inheritance +Each scope may inherit some configurations from its parent, and can +**always** override them by shadowing. + +*Any configuration that can be inherited by a workspace must be +inheritable by a site.* + +If not explicitly mentioned, a configuration is **always** +inheritable. + +#### Disabling Inherited Configurations +Any inherited configuration can be disabled in the current scope, and +a child may only inherit the enabled configurations of its parent. + +The state of an inherited configuration is configurable by clicking +the toggling buttons in the "Inherited" sections of each individual +configuration. + +### Appearance +`SiteCompanion` comes with the following appearance customization +options that can be inherited: +1. Theme: choose the theme of the extension popup and the toolbar. + The settings UI will use the global theme. +2. Toolbar Position: choose where the toolbar appears on known sites. +3. Always Use Default Env/Profile: enabling this option will disable + the environment and profile selection fields in the popup UI. +4. Empty Toolbar: let the toolbar hide or show a button called "Open + SiteCompanion" when the toolbar is empty in the current site. + +##### Global-Only Appearance Configurations +Some configurations are only applicable globally. + +1. Auto-hide toolbar on unknown sites: Turning on this option will + hide the toolbar when the current site is not known by + `SiteCompanion`. +2. Always show output buffer in popup UI: Turning this off will show + the output buffer even when the site is unknown. + +### API Keys (Global Only) +The keys for authenticating with the AI service provider. + +### API Configurations +Configures the details of AI service endpoints: +1. API Key: which API key to use. +2. API (Base) URL: what the URL to the AI service endpoint is. +3. Model Name: what model to use. + +*Workspaces and sites may only inherit API configurations of their +parents, and never create their own.* + +#### Advanced Mode +To accommodate different API request formats, `SiteCompanion` offers +"Advanced Mode" for a more customizable experience. + +In "Advanced Mode", you may directly edit the API endpoint URL and the +`JSON` template for requests. + +### Environments +Environments are bundles of API configuration and a system prompt. +They define the environment in which the task is running. + +An environment contains the following components: +1. API configuration: select the API configuration to use for this + environment. +2. System prompt: the piece of prompt that defines the profile of the + model. + +### Profiles +User profiles to give more context to the model about **who you are**. + +### Tasks +Prompts that tell the model **what to do**. + +#### Task Defaults +A task will be associated with a default environment and profile +combination for quick access. + +Of course, you may choose whichever environment and profile you want +to use in the popup UI. + +### Toolbar Shortcuts +Configures the predefined shortcuts that appear on the toolbar. + +Each shortcut is a combination of environment + profile + task. + +### Site-Specific Configurations +Some configurations are site-specific. They are there to configure how +`SiteCompanion` recognizes a site, the workspace the site belongs to, +and how it extracts text from it. + +The additional configuration options are: +1. URL Pattern: the globbing pattern to match URLs against for that + site configuration. +2. Workspace: the workspace that the site belongs to. +3. Site Text Selector: the configuration for the pattern used by + `SiteCompanion` to extract the text from the site. + +#### Selector Patterns +The supported selector patterns are: + * `{ kind: "css"; selector: string }` + * `{ kind: "cssAll"; selector: string; index: number }` + * `{ kind: "textScope"; text: string }` + * `{ kind: "anchoredCss"; anchor: { kind: "textScope"; text: string }; selector: string }` + + +## Planned Features +Nothing at the moment! + +Send a feature request to **Issues** if you'd like to request a new +feature! + +## Known Bugs +Nothing at the moment! + +Send a bug report to **Issues** if you find one! + +## Disclaimer +1. The extension is not responsible for your actions. **YOU** must be + responsible for your own actions. +2. `SiteCompanion` is free and will always be free; no party should + offer paid services for `SiteCompanion` users. +3. If a branch/fork diverges from the design of `SiteCompanion`, it + may not use its branding nor will it be merged into the official + version. +4. No donations or other monetization will affect feature requests or + bug fix priorities. diff --git a/sitecompanion/background.js b/sitecompanion/background.js index 0732851..b9d32b6 100644 --- a/sitecompanion/background.js +++ b/sitecompanion/background.js @@ -17,6 +17,8 @@ const DEFAULT_SETTINGS = { theme: "system", toolbarAutoHide: true, alwaysShowOutput: false, + alwaysUseDefaultEnvProfile: false, + emptyToolbarBehavior: "open", workspaces: [] }; diff --git a/sitecompanion/content.js b/sitecompanion/content.js index ef3371d..e414d5e 100644 --- a/sitecompanion/content.js +++ b/sitecompanion/content.js @@ -360,6 +360,22 @@ function resolveThemeValue(globalTheme, workspace, site) { return globalTheme || "system"; } +function normalizeEmptyToolbarBehavior(value, allowInherit = true) { + if (value === "hide" || value === "open") return value; + if (allowInherit && value === "inherit") return "inherit"; + return allowInherit ? "inherit" : "open"; +} + +function resolveEmptyToolbarBehavior(globalValue, workspace, site) { + const base = normalizeEmptyToolbarBehavior(globalValue, false); + const workspaceValue = normalizeEmptyToolbarBehavior( + workspace?.emptyToolbarBehavior + ); + const workspaceResolved = workspaceValue === "inherit" ? base : workspaceValue; + const siteValue = normalizeEmptyToolbarBehavior(site?.emptyToolbarBehavior); + return siteValue === "inherit" ? workspaceResolved : siteValue; +} + function resolveThemeMode(theme) { if (theme === "dark" || theme === "light") return theme; if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { @@ -393,10 +409,20 @@ function getToolbarThemeTokens(mode) { }; } -function createToolbar(shortcuts, position = "bottom-right", themeMode = "light", options = {}) { +function createToolbar( + shortcuts, + position = "bottom-right", + themeMode = "light", + options = {} +) { let toolbar = document.getElementById("sitecompanion-toolbar"); if (toolbar) toolbar.remove(); + const hasShortcuts = Array.isArray(shortcuts) && shortcuts.length > 0; + const showOpenButton = + options?.unknown || (!hasShortcuts && options?.emptyBehavior === "open"); + if (!hasShortcuts && !showOpenButton) return; + toolbar = document.createElement("div"); toolbar.id = "sitecompanion-toolbar"; @@ -437,7 +463,7 @@ function createToolbar(shortcuts, position = "bottom-right", themeMode = "light" color: ${tokens.ink}; `; - if (options?.unknown) { + if (showOpenButton) { const btn = document.createElement("button"); btn.type = "button"; btn.textContent = "Open SiteCompanion"; @@ -457,12 +483,6 @@ function createToolbar(shortcuts, position = "bottom-right", themeMode = "light" }); }); toolbar.appendChild(btn); - } else if (!shortcuts || !shortcuts.length) { - const label = document.createElement("span"); - label.textContent = "SiteCompanion"; - label.style.fontSize = "12px"; - label.style.color = tokens.muted; - toolbar.appendChild(label); } else { for (const shortcut of shortcuts) { const btn = document.createElement("button"); @@ -527,7 +547,8 @@ async function refreshToolbar() { presets = [], toolbarPosition = "bottom-right", theme = "system", - toolbarAutoHide = true + toolbarAutoHide = true, + emptyToolbarBehavior = "open" } = await chrome.storage.local.get([ "sites", "workspaces", @@ -535,7 +556,8 @@ async function refreshToolbar() { "presets", "toolbarPosition", "theme", - "toolbarAutoHide" + "toolbarAutoHide", + "emptyToolbarBehavior" ]); const currentUrl = window.location.href; const site = sites.find(s => matchUrl(currentUrl, s.urlPattern)); @@ -579,7 +601,19 @@ async function refreshToolbar() { : toolbarPosition; const resolvedTheme = resolveThemeValue(theme, workspace, site); const themeMode = resolveThemeMode(resolvedTheme); - createToolbar(siteShortcuts, resolvedPosition, themeMode); + const resolvedEmptyToolbarBehavior = resolveEmptyToolbarBehavior( + emptyToolbarBehavior, + workspace, + site + ); + if (!siteShortcuts.length && resolvedEmptyToolbarBehavior === "hide") { + const toolbar = document.getElementById("sitecompanion-toolbar"); + if (toolbar) toolbar.remove(); + return; + } + createToolbar(siteShortcuts, resolvedPosition, themeMode, { + emptyBehavior: resolvedEmptyToolbarBehavior + }); } catch (error) { const message = String(error?.message || ""); if (message.includes("Extension context invalidated")) { @@ -596,6 +630,7 @@ async function refreshToolbar() { let refreshTimer = null; +let contentChangeTimer = null; function scheduleToolbarRefresh() { if (refreshTimer) return; refreshTimer = window.setTimeout(() => { @@ -610,9 +645,22 @@ function scheduleToolbarRefresh() { }, 200); } +function scheduleContentChangeNotice() { + if (contentChangeTimer) return; + contentChangeTimer = window.setTimeout(() => { + contentChangeTimer = null; + chrome.runtime.sendMessage({ type: "SITE_CONTENT_CHANGED" }, () => { + if (chrome.runtime.lastError) { + return; + } + }); + }, 250); +} + const observer = new MutationObserver(() => { if (suppressObserver) return; scheduleToolbarRefresh(); + scheduleContentChangeNotice(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); diff --git a/sitecompanion/icon128.png b/sitecompanion/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..aba2554aef63c79c77bf8d2f0e01677265abee7a GIT binary patch literal 14495 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 x3NvXctRDXt00960lz$j200006Nkl"], @@ -22,5 +22,8 @@ "options_ui": { "page": "settings.html", "open_in_tab": true + }, + "icons": { + "128": "icon128.png" } } diff --git a/sitecompanion/popup.css b/sitecompanion/popup.css index f35dba3..a5c9f4b 100644 --- a/sitecompanion/popup.css +++ b/sitecompanion/popup.css @@ -47,6 +47,9 @@ body { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--ink); background: var(--page-bg); + --control-height: 30px; + --output-max-height-base: 276px; + --output-height-delta: 0px; } .title-block { @@ -119,7 +122,8 @@ label { select { width: 100%; - padding: 6px 8px; + height: var(--control-height); + padding: 0 8px; border-radius: 10px; border: 1px solid var(--border); background: var(--input-bg); @@ -138,6 +142,49 @@ select { margin: 0; } +.env-profile-summary { + display: none; + align-items: center; + gap: 8px; + font-size: 11px; + color: var(--muted); + margin-bottom: 8px; + font-style: italic; +} + +.env-profile-item { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +body.always-default-env-profile .selector-row { + display: none; +} + +body.always-default-env-profile .env-profile-summary { + display: flex; + margin-bottom: 8px; +} + +body.always-default-env-profile:not(.custom-task-mode) .config-block { + row-gap: 0; +} + +body.always-default-env-profile:not(.custom-task-mode) { + --output-max-height-base: 309px; +} + +body.custom-task-mode .env-profile-summary { + display: none; +} + +body.custom-task-mode.always-default-env-profile .selector-row { + display: flex; +} + .task-row { display: flex; align-items: flex-end; @@ -145,7 +192,7 @@ select { } .task-row button { - padding: 6px 15px; + padding: 0 15px; } .task-row .task-field { @@ -157,6 +204,37 @@ select { min-width: 0; } +.custom-task-row { + align-items: stretch; +} + +.custom-task-field { + flex: 1; + margin: 0; +} + +.custom-task-field textarea { + width: 100%; + padding: 8px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--input-bg); + color: var(--input-fg); + font-size: 12px; + resize: vertical; + min-height: 52px; +} + +.custom-task-actions { + display: flex; + flex-direction: column; + gap: 6px; +} + +.custom-task-actions button { + width: 100%; +} + .hidden { display: none !important; } @@ -221,11 +299,18 @@ button { font-family: inherit; border: none; border-radius: 10px; - padding: 6px 10px; + padding: 0 10px; cursor: pointer; transition: transform 0.15s ease, box-shadow 0.15s ease; } +button.control-btn { + height: var(--control-height); + display: inline-flex; + align-items: center; + justify-content: center; +} + button:disabled { opacity: 0.5; cursor: not-allowed; @@ -287,14 +372,14 @@ button:active { padding: 8px; background: var(--output-bg); min-height: 210px; - max-height: 280px; + max-height: calc(var(--output-max-height-base) - var(--output-height-delta)); overflow: hidden; } .output-body { margin: 0; word-break: break-word; - max-height: 260px; + max-height: calc(var(--output-max-height-base) - var(--output-height-delta) - 20px); overflow-y: auto; font-size: 11px; line-height: 1.45; @@ -384,6 +469,10 @@ button:active { gap: 8px; } +.footer.compact { + justify-content: flex-end; +} + .footer-left { display: flex; align-items: center; diff --git a/sitecompanion/popup.html b/sitecompanion/popup.html index 4ff041c..3698a9d 100644 --- a/sitecompanion/popup.html +++ b/sitecompanion/popup.html @@ -20,8 +20,8 @@
- - + +
@@ -43,8 +43,8 @@
- - + +
@@ -65,19 +65,45 @@ -
+
+
+ ENV: +
+
+ PROFILE: +
+
+
- - + +
+ + +
+
+
Site Text: 0 chars - Task: 0 chars + Total: 0 chars Idle
@@ -88,9 +114,15 @@
diff --git a/sitecompanion/popup.js b/sitecompanion/popup.js index f114042..a388a7b 100644 --- a/sitecompanion/popup.js +++ b/sitecompanion/popup.js @@ -3,6 +3,16 @@ const abortBtn = document.getElementById("abortBtn"); const taskSelect = document.getElementById("taskSelect"); const envSelect = document.getElementById("envSelect"); const profileSelect = document.getElementById("profileSelect"); +const envProfileSummary = document.getElementById("envProfileSummary"); +const envSummaryValue = document.getElementById("envSummaryValue"); +const profileSummaryValue = document.getElementById("profileSummaryValue"); +const customTaskBtn = document.getElementById("customTaskBtn"); +const normalTaskBtn = document.getElementById("normalTaskBtn"); +const customTaskInput = document.getElementById("customTaskInput"); +const normalTaskRow = document.getElementById("normalTaskRow"); +const customTaskRow = document.getElementById("customTaskRow"); +const taskActions = document.getElementById("taskActions"); +const taskActionsSlot = document.getElementById("taskActionsSlot"); const outputEl = document.getElementById("output"); const statusEl = document.getElementById("status"); const postingCountEl = document.getElementById("postingCount"); @@ -12,6 +22,8 @@ const copyRenderedBtn = document.getElementById("copyRenderedBtn"); const copyRawBtn = document.getElementById("copyRawBtn"); const clearOutputBtn = document.getElementById("clearOutputBtn"); const outputSection = document.querySelector(".output"); +const footerLeft = document.querySelector(".footer-left"); +const footer = document.querySelector(".footer"); const OUTPUT_STORAGE_KEY = "lastOutput"; const AUTO_RUN_KEY = "autoRunDefaultTask"; @@ -20,6 +32,8 @@ const LAST_TASK_KEY = "lastSelectedTaskId"; const LAST_ENV_KEY = "lastSelectedEnvId"; const LAST_PROFILE_KEY = "lastSelectedProfileId"; const POPUP_DRAFT_KEY = "popupDraft"; +const CUSTOM_TASK_MODE_KEY = "customTaskMode"; +const CUSTOM_TASK_TEXT_KEY = "customTaskText"; const unknownSiteState = document.getElementById("unknownSiteState"); const extractionReviewState = document.getElementById("extractionReviewState"); @@ -57,7 +71,12 @@ const state = { selectedTaskId: "", selectedEnvId: "", selectedProfileId: "", - alwaysShowOutput: false + alwaysShowOutput: false, + alwaysUseDefaultEnvProfile: false, + activeTabId: null, + pendingConfigRefresh: false, + customTaskMode: false, + customTaskText: "" }; async function switchState(stateName) { @@ -118,6 +137,7 @@ function applyPopupDraft(draft) { } else if (typeof draft.siteTextSelector === "string") { state.siteTextTarget = { kind: "css", selector: draft.siteTextSelector }; } + updateCounts(); } function matchUrl(url, pattern) { @@ -668,6 +688,46 @@ function resolveThemeForPopup(baseTheme) { return baseTheme || "system"; } +function resolveAppearanceToggleValue(value, fallback) { + if (value === "enabled") return true; + if (value === "disabled") return false; + if (value === "inherit" || value === null || value === undefined) { + return Boolean(fallback); + } + if (typeof value === "boolean") return value; + return Boolean(fallback); +} + +function resolveAlwaysUseDefaultEnvProfile(baseSetting, workspace, site) { + const resolvedBase = resolveAppearanceToggleValue(baseSetting, false); + const workspaceResolved = resolveAppearanceToggleValue( + workspace?.alwaysUseDefaultEnvProfile, + resolvedBase + ); + return resolveAppearanceToggleValue( + site?.alwaysUseDefaultEnvProfile, + workspaceResolved + ); +} + +function updateEnvProfileSummary() { + if (!envSummaryValue || !profileSummaryValue) return; + const env = getSelectedEnv(); + const profile = getSelectedProfile(); + envSummaryValue.textContent = env ? env.name || "Default" : "None"; + profileSummaryValue.textContent = profile ? profile.name || "Default" : "None"; +} + +function applyAlwaysUseDefaultEnvProfileState() { + document.body.classList.toggle( + "always-default-env-profile", + state.alwaysUseDefaultEnvProfile + ); + updateEnvSelectState(); + updateProfileSelectState(); + updateEnvProfileSummary(); +} + function setAnalyzing(isAnalyzing) { state.isAnalyzing = isAnalyzing; runBtn.disabled = isAnalyzing; @@ -677,6 +737,10 @@ function setAnalyzing(isAnalyzing) { updateTaskSelectState(); updateEnvSelectState(); updateProfileSelectState(); + if (!isAnalyzing && state.pendingConfigRefresh) { + state.pendingConfigRefresh = false; + scheduleConfigRefresh(); + } } function updateOutputVisibility() { @@ -684,14 +748,175 @@ function updateOutputVisibility() { const shouldHide = state.currentPopupState !== "normal" && !state.alwaysShowOutput; outputSection.classList.toggle("hidden", shouldHide); + footerLeft?.classList.toggle("hidden", shouldHide); + footer?.classList.toggle("compact", shouldHide); +} + +async function persistCustomTaskState() { + await chrome.storage.local.set({ + [CUSTOM_TASK_MODE_KEY]: state.customTaskMode, + [CUSTOM_TASK_TEXT_KEY]: state.customTaskText + }); +} + +function setCustomTaskMode(enabled, { persist = true } = {}) { + state.customTaskMode = Boolean(enabled); + document.body.classList.toggle("custom-task-mode", state.customTaskMode); + if (state.customTaskMode) { + if (normalTaskRow) { + const measured = measureRowHeight(normalTaskRow); + if (measured) normalTaskRowHeight = measured; + normalTaskRow.classList.add("hidden"); + } + customTaskRow?.classList.remove("hidden"); + if (taskActionsSlot && taskActions) { + taskActionsSlot.appendChild(taskActions); + } + if (customTaskInput) { + customTaskInput.value = state.customTaskText || ""; + customTaskInput.focus(); + } + window.requestAnimationFrame(() => { + if (customTaskRow) { + const measured = measureRowHeight(customTaskRow); + if (measured) customTaskRowHeight = measured; + } + updateOutputHeightDelta(); + }); + } else { + customTaskRow?.classList.add("hidden"); + if (normalTaskRow) { + normalTaskRow.classList.remove("hidden"); + } + if (normalTaskRow && taskActions) { + normalTaskRow.appendChild(taskActions); + } + window.requestAnimationFrame(() => { + if (normalTaskRow) { + const measured = measureRowHeight(normalTaskRow); + if (measured) normalTaskRowHeight = measured; + } + updateOutputHeightDelta(); + }); + } + updatePromptCount(); + updateEnvSelectState(); + updateProfileSelectState(); + if (persist) { + void persistCustomTaskState(); + } +} + +function getSelectedTask() { + if (state.forcedTask) return state.forcedTask; + const selectedId = taskSelect?.value || state.selectedTaskId; + return state.tasks.find((item) => item.id === selectedId) || state.tasks[0] || null; +} + +function getSelectedProfile() { + const selectedId = profileSelect?.value || state.selectedProfileId; + return ( + state.profiles.find((item) => item.id === selectedId) || + state.profiles[0] || + null + ); +} + +function getSelectedEnv() { + const selectedId = envSelect?.value || state.selectedEnvId; + return state.envs.find((item) => item.id === selectedId) || state.envs[0] || null; +} + +function buildTotalPromptText() { + const task = getSelectedTask(); + const profile = getSelectedProfile(); + const env = getSelectedEnv(); + const systemPrompt = env?.systemPrompt || ""; + const customText = (state.customTaskText || "").trim(); + const taskText = + state.customTaskMode && !state.forcedTask ? customText : task?.text || ""; + const userPrompt = buildUserMessage( + profile?.text || "", + taskText, + state.siteText || "" + ); + return systemPrompt ? `${systemPrompt}\n\n${userPrompt}` : userPrompt; } function updateSiteTextCount() { - postingCountEl.textContent = `Site Text: ${state.siteText.length} chars`; + const length = (state.siteText || "").length; + postingCountEl.textContent = `Site Text: ${length} chars`; } function updatePromptCount(count) { - promptCountEl.textContent = `Task: ${count} chars`; + const total = + typeof count === "number" ? count : buildTotalPromptText().length; + promptCountEl.textContent = `Total: ${total} chars`; +} + +function updateCounts() { + updateSiteTextCount(); + updatePromptCount(); +} + +let siteContentRefreshTimer = null; +function scheduleSiteContentRefresh() { + if (siteContentRefreshTimer) return; + siteContentRefreshTimer = window.setTimeout(() => { + siteContentRefreshTimer = null; + void refreshSiteContentCounts(); + }, 250); +} + +let configRefreshTimer = null; +function scheduleConfigRefresh() { + if (state.isAnalyzing) { + state.pendingConfigRefresh = true; + return; + } + if (configRefreshTimer) return; + configRefreshTimer = window.setTimeout(() => { + configRefreshTimer = null; + void loadConfig(); + }, 250); +} + +let normalTaskRowHeight = null; +let customTaskRowHeight = null; + +function measureRowHeight(row) { + if (!row) return 0; + return row.getBoundingClientRect().height || 0; +} + +function updateOutputHeightDelta() { + const baseHeight = normalTaskRowHeight || measureRowHeight(normalTaskRow); + if (!baseHeight) return; + if (!state.customTaskMode) { + document.body.style.setProperty("--output-height-delta", "0px"); + return; + } + const customHeight = customTaskRowHeight || measureRowHeight(customTaskRow); + const delta = Math.max(0, customHeight - baseHeight); + document.body.style.setProperty("--output-height-delta", `${Math.round(delta)}px`); +} + +async function refreshSiteContentCounts() { + if (state.isAnalyzing) return; + if (state.currentPopupState !== "normal") return; + if (!state.siteTextTarget) return; + try { + const response = await sendToActiveTab({ + type: "EXTRACT_BY_SELECTOR", + target: state.siteTextTarget + }); + if (!response?.ok) return; + state.siteText = response.extracted || ""; + state.siteTextTarget = response.target || state.siteTextTarget; + updateCounts(); + } catch { + // Ignore refresh failures; counts will update on next explicit extract. + } } function renderTasks(tasks) { @@ -726,6 +951,7 @@ function renderEnvironments(envs) { option.value = ""; envSelect.appendChild(option); updateEnvSelectState(); + updateEnvProfileSummary(); return; } @@ -736,6 +962,7 @@ function renderEnvironments(envs) { envSelect.appendChild(option); } updateEnvSelectState(); + updateEnvProfileSummary(); } function updateTaskSelectState() { @@ -745,7 +972,10 @@ function updateTaskSelectState() { function updateEnvSelectState() { const hasEnvs = state.envs.length > 0; - envSelect.disabled = state.isAnalyzing || !hasEnvs; + envSelect.disabled = + state.isAnalyzing || + !hasEnvs || + (state.alwaysUseDefaultEnvProfile && !state.customTaskMode); } function renderProfiles(profiles) { @@ -758,6 +988,7 @@ function renderProfiles(profiles) { option.value = ""; profileSelect.appendChild(option); updateProfileSelectState(); + updateEnvProfileSummary(); return; } @@ -768,11 +999,15 @@ function renderProfiles(profiles) { profileSelect.appendChild(option); } updateProfileSelectState(); + updateEnvProfileSummary(); } function updateProfileSelectState() { const hasProfiles = state.profiles.length > 0; - profileSelect.disabled = state.isAnalyzing || !hasProfiles; + profileSelect.disabled = + state.isAnalyzing || + !hasProfiles || + (state.alwaysUseDefaultEnvProfile && !state.customTaskMode); } function getTaskDefaultEnvId(task) { @@ -792,6 +1027,8 @@ function setEnvironmentSelection(envId) { envSelect.value = target; } state.selectedEnvId = target; + updatePromptCount(); + updateEnvProfileSummary(); } function setProfileSelection(profileId) { @@ -803,6 +1040,8 @@ function setProfileSelection(profileId) { profileSelect.value = target; } state.selectedProfileId = target; + updatePromptCount(); + updateEnvProfileSummary(); } function selectTask(taskId, { resetEnv } = { resetEnv: false }) { @@ -814,6 +1053,7 @@ function selectTask(taskId, { resetEnv } = { resetEnv: false }) { setEnvironmentSelection(getTaskDefaultEnvId(task)); setProfileSelection(getTaskDefaultProfileId(task)); } + updatePromptCount(); } async function persistSelections() { @@ -900,6 +1140,7 @@ function ensurePort() { async function loadConfig() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const currentUrl = tabs[0]?.url || ""; + state.activeTabId = tabs[0]?.id || null; const { lastPopupState, [POPUP_DRAFT_KEY]: popupDraft } = await getStorage([ "lastPopupState", @@ -926,9 +1167,12 @@ async function loadConfig() { "sites", "theme", "alwaysShowOutput", + "alwaysUseDefaultEnvProfile", LAST_TASK_KEY, LAST_ENV_KEY, - LAST_PROFILE_KEY + LAST_PROFILE_KEY, + CUSTOM_TASK_MODE_KEY, + CUSTOM_TASK_TEXT_KEY ]); const tasks = normalizeConfigList(stored.tasks); const envs = normalizeConfigList(stored.envConfigs); @@ -968,12 +1212,23 @@ async function loadConfig() { state.currentWorkspace = activeWorkspace; currentWorkspaceName.textContent = activeWorkspace.name || "Global"; } + if (state.currentSite && !state.siteTextTarget) { + state.siteTextTarget = normalizeStoredExtractTarget(state.currentSite); + } if (stored.theme) { state.globalTheme = stored.theme; } state.alwaysShowOutput = Boolean(stored.alwaysShowOutput); + state.alwaysUseDefaultEnvProfile = resolveAlwaysUseDefaultEnvProfile( + stored.alwaysUseDefaultEnvProfile, + activeWorkspace, + activeSite + ); applyTheme(resolveThemeForPopup(state.globalTheme)); updateOutputVisibility(); + applyAlwaysUseDefaultEnvProfileState(); + state.customTaskMode = Boolean(stored[CUSTOM_TASK_MODE_KEY]); + state.customTaskText = stored[CUSTOM_TASK_TEXT_KEY] || ""; const effectiveEnvs = resolveEffectiveList( envs, @@ -1000,11 +1255,16 @@ async function loadConfig() { renderTasks(effectiveTasks); renderEnvironments(effectiveEnvs); renderProfiles(effectiveProfiles); + if (customTaskInput) { + customTaskInput.value = state.customTaskText; + } + setCustomTaskMode(state.customTaskMode, { persist: false }); if (!effectiveTasks.length) { state.selectedTaskId = ""; setEnvironmentSelection(effectiveEnvs[0]?.id || ""); setProfileSelection(effectiveProfiles[0]?.id || ""); + updateCounts(); return; } @@ -1014,22 +1274,24 @@ async function loadConfig() { const initialTaskId = effectiveTasks.some((task) => task.id === storedTaskId) ? storedTaskId : effectiveTasks[0].id; - selectTask(initialTaskId, { resetEnv: false }); + selectTask(initialTaskId, { resetEnv: state.alwaysUseDefaultEnvProfile }); const task = effectiveTasks.find((item) => item.id === initialTaskId); - if (storedEnvId && effectiveEnvs.some((env) => env.id === storedEnvId)) { - setEnvironmentSelection(storedEnvId); - } else { - setEnvironmentSelection(getTaskDefaultEnvId(task)); - } + if (!state.alwaysUseDefaultEnvProfile) { + if (storedEnvId && effectiveEnvs.some((env) => env.id === storedEnvId)) { + setEnvironmentSelection(storedEnvId); + } else { + setEnvironmentSelection(getTaskDefaultEnvId(task)); + } - if ( - storedProfileId && - effectiveProfiles.some((profile) => profile.id === storedProfileId) - ) { - setProfileSelection(storedProfileId); - } else { - setProfileSelection(getTaskDefaultProfileId(task)); + if ( + storedProfileId && + effectiveProfiles.some((profile) => profile.id === storedProfileId) + ) { + setProfileSelection(storedProfileId); + } else { + setProfileSelection(getTaskDefaultProfileId(task)); + } } if ( @@ -1040,6 +1302,10 @@ async function loadConfig() { await persistSelections(); } + updateCounts(); + if (state.currentSite) { + await refreshSiteContentCounts(); + } maybeRunDefaultTask(); } @@ -1068,8 +1334,7 @@ async function handleExtract() { state.siteText = response.extracted || ""; state.siteTextTarget = response.target || target; - updateSiteTextCount(); - updatePromptCount(0); + updateCounts(); setStatus("Text extracted."); return true; } catch (error) { @@ -1094,13 +1359,18 @@ async function handleAnalyze() { const taskId = taskSelect.value; const forcedTask = state.forcedTask; const task = forcedTask || state.tasks.find((item) => item.id === taskId); + const useCustomTask = state.customTaskMode && !forcedTask; if (forcedTask) { state.forcedTask = null; } - if (!task) { + if (!useCustomTask && !task) { setStatus("Select a task."); return; } + if (state.alwaysUseDefaultEnvProfile && !forcedTask && !state.customTaskMode) { + setEnvironmentSelection(getTaskDefaultEnvId(task)); + setProfileSelection(getTaskDefaultProfileId(task)); + } const { apiKeys = [], @@ -1139,6 +1409,12 @@ async function handleAnalyze() { } const resolvedSystemPrompt = activeEnv.systemPrompt ?? systemPrompt ?? ""; + const customTaskText = (state.customTaskText || "").trim(); + const resolvedTaskText = useCustomTask ? customTaskText : task?.text || ""; + if (useCustomTask && !resolvedTaskText) { + setStatus("Enter a custom task."); + return; + } const resolvedApiConfigId = activeEnv.apiConfigId || activeApiConfigId || resolvedConfigs[0]?.id || ""; const activeConfig = @@ -1194,12 +1470,7 @@ async function handleAnalyze() { } } - const promptText = buildUserMessage( - profileText, - task.text || "", - state.siteText - ); - updatePromptCount(promptText.length); + updatePromptCount(); state.outputRaw = ""; renderOutput(); @@ -1220,7 +1491,7 @@ async function handleAnalyze() { model: resolvedModel, systemPrompt: resolvedSystemPrompt, profileText, - taskText: task.text || "", + taskText: resolvedTaskText, siteText: state.siteText, tabId: tab.id } @@ -1298,6 +1569,7 @@ async function runMinimalExtraction(text, minLength = 5) { state.siteText = response.extracted; state.siteTextTarget = response.target || { kind: "textScope", text: trimmed }; extractedPreview.textContent = state.siteText; + updateCounts(); await fillSiteDefaultsFromTab(); switchState("review"); await persistPopupDraft(); @@ -1336,6 +1608,7 @@ extractFullBtn.addEventListener("click", async () => { state.siteText = response.extracted; state.siteTextTarget = target; extractedPreview.textContent = state.siteText; + updateCounts(); await fillSiteDefaultsFromTab(); switchState("review"); await persistPopupDraft(); @@ -1372,6 +1645,7 @@ retryExtractBtn.addEventListener("click", () => { if (workspaceSelect) workspaceSelect.value = "global"; state.siteText = ""; state.siteTextTarget = null; + updateCounts(); setMinimalStatus(""); void clearPopupDraft(); setStatus("Ready."); @@ -1427,10 +1701,24 @@ confirmSiteBtn.addEventListener("click", async () => { currentWorkspaceName.textContent = state.currentWorkspace.name || "Global"; await loadConfig(); await switchState("normal"); - updateSiteTextCount(); + updateCounts(); setStatus("Site saved."); }); +customTaskBtn?.addEventListener("click", () => { + setCustomTaskMode(true); +}); + +normalTaskBtn?.addEventListener("click", () => { + setCustomTaskMode(false); +}); + +customTaskInput?.addEventListener("input", () => { + state.customTaskText = customTaskInput.value || ""; + updatePromptCount(); + void persistCustomTaskState(); +}); + runBtn.addEventListener("click", handleExtractAndAnalyze); abortBtn.addEventListener("click", handleAbort); settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage()); @@ -1450,8 +1738,7 @@ profileSelect.addEventListener("change", () => { void persistSelections(); }); -updateSiteTextCount(); -updatePromptCount(0); +updateCounts(); renderOutput(); setAnalyzing(false); void loadTheme(); @@ -1475,6 +1762,7 @@ async function loadShortcutRunRequest() { state.shortcutRunPending = true; await chrome.storage.local.remove(SHORTCUT_RUN_KEY); + setCustomTaskMode(false); if (!state.tasks.length) { await loadConfig(); @@ -1538,7 +1826,13 @@ async function loadShortcutRunRequest() { await persistSelections(); state.autoRunPending = false; state.shortcutRunPending = false; - void handleExtractAndAnalyze(); + void handleExtractAndAnalyze().finally(() => { + if (!state.alwaysUseDefaultEnvProfile) return; + const selectedTask = getSelectedTask(); + if (!selectedTask) return; + setEnvironmentSelection(getTaskDefaultEnvId(selectedTask)); + setProfileSelection(getTaskDefaultProfileId(selectedTask)); + }); } async function loadAutoRunRequest() { @@ -1597,4 +1891,28 @@ chrome.storage.onChanged.addListener((changes) => { state.alwaysShowOutput = Boolean(changes.alwaysShowOutput.newValue); updateOutputVisibility(); } + + const configKeys = [ + "tasks", + "envConfigs", + "profiles", + "shortcuts", + "workspaces", + "sites", + "theme", + "alwaysShowOutput", + "alwaysUseDefaultEnvProfile" + ]; + if (configKeys.some((key) => changes[key])) { + scheduleConfigRefresh(); + } +}); + +chrome.runtime.onMessage.addListener((message, sender) => { + if (message?.type !== "SITE_CONTENT_CHANGED") return; + const senderTabId = sender?.tab?.id || null; + if (state.activeTabId && senderTabId && senderTabId !== state.activeTabId) { + return; + } + scheduleSiteContentRefresh(); }); diff --git a/sitecompanion/settings.css b/sitecompanion/settings.css index 498d13e..d7c9cb7 100644 --- a/sitecompanion/settings.css +++ b/sitecompanion/settings.css @@ -749,6 +749,10 @@ button:active { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.inline-fields.four { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + .inline-fields .field { margin-bottom: 0; } diff --git a/sitecompanion/settings.html b/sitecompanion/settings.html index b00c1f8..0d30a3c 100644 --- a/sitecompanion/settings.html +++ b/sitecompanion/settings.html @@ -103,7 +103,7 @@
-
+
+
+ + +
+
+ + +
diff --git a/sitecompanion/settings.js b/sitecompanion/settings.js index 1e9689b..8203205 100644 --- a/sitecompanion/settings.js +++ b/sitecompanion/settings.js @@ -19,8 +19,14 @@ const statusSidebarEl = document.getElementById("statusSidebar"); const sidebarErrorsEl = document.getElementById("sidebarErrors"); const themeSelect = document.getElementById("themeSelect"); const toolbarPositionSelect = document.getElementById("toolbarPositionSelect"); +const emptyToolbarBehaviorSelect = document.getElementById( + "emptyToolbarBehaviorSelect" +); const toolbarAutoHide = document.getElementById("toolbarAutoHide"); const alwaysShowOutput = document.getElementById("alwaysShowOutput"); +const alwaysUseDefaultEnvProfileSelect = document.getElementById( + "alwaysUseDefaultEnvProfileSelect" +); const globalSitesContainer = document.getElementById("globalSites"); const toc = document.querySelector(".toc"); const tocResizer = document.getElementById("tocResizer"); @@ -453,8 +459,14 @@ function buildSettingsSnapshot() { toolbarPosition: toolbarPositionSelect ? toolbarPositionSelect.value : "bottom-right", + emptyToolbarBehavior: emptyToolbarBehaviorSelect + ? emptyToolbarBehaviorSelect.value + : "open", toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true, - alwaysShowOutput: alwaysShowOutput ? alwaysShowOutput.checked : false + alwaysShowOutput: alwaysShowOutput ? alwaysShowOutput.checked : false, + alwaysUseDefaultEnvProfile: alwaysUseDefaultEnvProfileSelect + ? alwaysUseDefaultEnvProfileSelect.value === "enabled" + : false }); } @@ -519,6 +531,35 @@ function scheduleSidebarErrors() { }); } +let tocUpdateFrame = null; +function scheduleTocUpdate() { + if (!toc) return; + if (tocUpdateFrame) return; + tocUpdateFrame = requestAnimationFrame(() => { + tocUpdateFrame = null; + updateToc(collectWorkspaces(), collectSites()); + }); +} + +const TOC_NAME_INPUT_SELECTOR = [ + ".api-key-name", + ".api-config-name", + ".env-config-name", + ".profile-name", + ".task-name", + ".shortcut-name", + ".workspace-name", + ".site-name", + ".site-pattern" +].join(", "); + +function handleTocNameInput(event) { + const target = event.target; + if (!(target instanceof Element)) return; + if (!target.matches(TOC_NAME_INPUT_SELECTOR)) return; + scheduleTocUpdate(); +} + function renderGlobalSitesList(sites) { if (!globalSitesContainer) return; globalSitesContainer.innerHTML = ""; @@ -1230,6 +1271,7 @@ function updateApiConfigControls() { if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; }); scheduleSidebarErrors(); + scheduleTocUpdate(); } function buildApiKeyCard(entry) { @@ -1404,6 +1446,7 @@ function updateApiKeyControls() { if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; }); scheduleSidebarErrors(); + scheduleTocUpdate(); } function updateApiConfigKeyOptions() { @@ -1630,6 +1673,7 @@ function updateEnvControls(container = envConfigsContainer) { if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; }); scheduleSidebarErrors(); + scheduleTocUpdate(); } function updateTaskEnvOptionsForContainer(container, envs, allEnvsById) { @@ -1879,6 +1923,7 @@ function updateProfileControls(container = profilesContainer) { if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; }); scheduleSidebarErrors(); + scheduleTocUpdate(); } function updateTaskProfileOptionsForContainer(container, profiles, allProfilesById) { @@ -1952,7 +1997,9 @@ function updateEnvApiOptionsForContainer(container, apiConfigs) { if (!container) return; const selects = container.querySelectorAll(".env-config-api-select"); selects.forEach((select) => { - select.dataset.preferred = select.value; + if (select.value) { + select.dataset.preferred = select.value; + } populateSelect(select, apiConfigs, "No API configs configured"); }); } @@ -2195,15 +2242,21 @@ function updateShortcutOptionsForContainer(container, options = {}) { const profileSelect = card.querySelector(".shortcut-profile"); const taskSelect = card.querySelector(".shortcut-task"); if (envSelect) { - envSelect.dataset.preferred = envSelect.value; + if (envSelect.value) { + envSelect.dataset.preferred = envSelect.value; + } populateSelect(envSelect, envs, "No environments configured"); } if (profileSelect) { - profileSelect.dataset.preferred = profileSelect.value; + if (profileSelect.value) { + profileSelect.dataset.preferred = profileSelect.value; + } populateSelect(profileSelect, profiles, "No profiles configured"); } if (taskSelect) { - taskSelect.dataset.preferred = taskSelect.value; + if (taskSelect.value) { + taskSelect.dataset.preferred = taskSelect.value; + } populateSelect(taskSelect, tasks, "No tasks configured"); } }); @@ -2246,6 +2299,10 @@ function collectWorkspaces() { const nameInput = card.querySelector(".workspace-name"); const themeSelect = card.querySelector(".appearance-theme"); const toolbarSelect = card.querySelector(".appearance-toolbar-position"); + const defaultEnvProfileSelect = card.querySelector( + ".appearance-default-env-profile" + ); + const emptyToolbarSelect = card.querySelector(".appearance-empty-toolbar"); // Collect nested resources const envsContainer = card.querySelector(".workspace-envs"); @@ -2269,6 +2326,12 @@ function collectWorkspaces() { name: (nameInput?.value || "Untitled Workspace").trim(), theme: themeSelect?.value || "inherit", toolbarPosition: toolbarSelect?.value || "inherit", + alwaysUseDefaultEnvProfile: normalizeAppearanceToggle( + defaultEnvProfileSelect?.value + ), + emptyToolbarBehavior: normalizeEmptyToolbarBehavior( + emptyToolbarSelect?.value + ), envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [], profiles: profilesContainer ? collectProfiles(profilesContainer) : [], tasks: tasksContainer ? collectTasks(tasksContainer) : [], @@ -2381,12 +2444,196 @@ function renderWorkspaceSection(title, containerClass, items, builder, newItemFa summaryRight.appendChild(addBtn); body.appendChild(listContainer); details.appendChild(body); - + return details; } +const THEME_LABELS = { + system: "System", + light: "Light", + dark: "Dark" +}; + +const TOOLBAR_POSITION_LABELS = { + "bottom-right": "Bottom Right", + "bottom-left": "Bottom Left", + "top-right": "Top Right", + "top-left": "Top Left", + "bottom-center": "Bottom Center" +}; + +const EMPTY_TOOLBAR_BEHAVIOR_LABELS = { + hide: "Hide Toolbar", + open: 'Show "Open SiteCompanion"' +}; + +function normalizeAppearanceToggle(value) { + if (value === "inherit" || value === "enabled" || value === "disabled") { + return value; + } + if (value === true) return "enabled"; + if (value === false) return "disabled"; + return "inherit"; +} + +function normalizeEmptyToolbarBehavior(value, allowInherit = true) { + if (value === "hide" || value === "open") return value; + if (allowInherit && value === "inherit") return "inherit"; + return allowInherit ? "inherit" : "open"; +} + +function resolveAppearanceToggleValue(value, fallback) { + const normalized = normalizeAppearanceToggle(value); + if (normalized === "inherit") return Boolean(fallback); + return normalized === "enabled"; +} + +function getThemeLabel(value) { + return THEME_LABELS[value] || String(value || "System"); +} + +function getToolbarPositionLabel(value) { + return TOOLBAR_POSITION_LABELS[value] || String(value || "Bottom Right"); +} + +function getDefaultEnvProfileLabel(value) { + return value ? "Enabled" : "Disabled"; +} + +function getEmptyToolbarBehaviorLabel(value) { + return EMPTY_TOOLBAR_BEHAVIOR_LABELS[value] || "Hide Toolbar"; +} + +function getGlobalAppearanceConfig() { + return { + theme: themeSelect?.value || "system", + toolbarPosition: toolbarPositionSelect?.value || "bottom-right", + alwaysUseDefaultEnvProfile: resolveAppearanceToggleValue( + alwaysUseDefaultEnvProfileSelect?.value, + false + ), + emptyToolbarBehavior: normalizeEmptyToolbarBehavior( + emptyToolbarBehaviorSelect?.value, + false + ) + }; +} + +function updateAppearanceInheritedHint(selectEl, hintEl, label) { + if (!selectEl || !hintEl) return; + if (selectEl.value !== "inherit") { + hintEl.textContent = "Not inheriting"; + hintEl.classList.remove("hidden"); + return; + } + hintEl.textContent = `Inherited: ${label}`; + hintEl.classList.remove("hidden"); +} + +function updateAppearanceInheritanceIndicators() { + const global = getGlobalAppearanceConfig(); + const workspaceCards = document.querySelectorAll(".workspace-card"); + const workspaceAppearance = new Map(); + + workspaceCards.forEach((card) => { + const themeSelect = card.querySelector(".appearance-theme"); + const toolbarSelect = card.querySelector(".appearance-toolbar-position"); + const defaultSelect = card.querySelector(".appearance-default-env-profile"); + const emptyToolbarSelect = card.querySelector(".appearance-empty-toolbar"); + const themeValue = themeSelect?.value || "inherit"; + const toolbarValue = toolbarSelect?.value || "inherit"; + const defaultValue = defaultSelect?.value || "inherit"; + const emptyToolbarValue = normalizeEmptyToolbarBehavior( + emptyToolbarSelect?.value || "inherit" + ); + const resolvedTheme = + themeValue === "inherit" ? global.theme : themeValue; + const resolvedToolbar = + toolbarValue === "inherit" ? global.toolbarPosition : toolbarValue; + const resolvedDefault = resolveAppearanceToggleValue( + defaultValue, + global.alwaysUseDefaultEnvProfile + ); + const resolvedEmptyToolbar = + emptyToolbarValue === "inherit" + ? global.emptyToolbarBehavior + : emptyToolbarValue; + workspaceAppearance.set(card.dataset.id, { + theme: resolvedTheme, + toolbarPosition: resolvedToolbar, + alwaysUseDefaultEnvProfile: resolvedDefault, + emptyToolbarBehavior: resolvedEmptyToolbar + }); + + updateAppearanceInheritedHint( + themeSelect, + card.querySelector('.appearance-inherited[data-appearance-key="theme"]'), + getThemeLabel(global.theme) + ); + updateAppearanceInheritedHint( + toolbarSelect, + card.querySelector( + '.appearance-inherited[data-appearance-key="toolbarPosition"]' + ), + getToolbarPositionLabel(global.toolbarPosition) + ); + updateAppearanceInheritedHint( + defaultSelect, + card.querySelector( + '.appearance-inherited[data-appearance-key="alwaysUseDefaultEnvProfile"]' + ), + getDefaultEnvProfileLabel(global.alwaysUseDefaultEnvProfile) + ); + updateAppearanceInheritedHint( + emptyToolbarSelect, + card.querySelector( + '.appearance-inherited[data-appearance-key="emptyToolbarBehavior"]' + ), + getEmptyToolbarBehaviorLabel(global.emptyToolbarBehavior) + ); + }); + + const siteCards = document.querySelectorAll(".site-card"); + siteCards.forEach((card) => { + const workspaceId = card.querySelector(".site-workspace")?.value || "global"; + const resolved = + workspaceAppearance.get(workspaceId) || global; + updateAppearanceInheritedHint( + card.querySelector(".appearance-theme"), + card.querySelector('.appearance-inherited[data-appearance-key="theme"]'), + getThemeLabel(resolved.theme) + ); + updateAppearanceInheritedHint( + card.querySelector(".appearance-toolbar-position"), + card.querySelector( + '.appearance-inherited[data-appearance-key="toolbarPosition"]' + ), + getToolbarPositionLabel(resolved.toolbarPosition) + ); + updateAppearanceInheritedHint( + card.querySelector(".appearance-default-env-profile"), + card.querySelector( + '.appearance-inherited[data-appearance-key="alwaysUseDefaultEnvProfile"]' + ), + getDefaultEnvProfileLabel(resolved.alwaysUseDefaultEnvProfile) + ); + updateAppearanceInheritedHint( + card.querySelector(".appearance-empty-toolbar"), + card.querySelector( + '.appearance-inherited[data-appearance-key="emptyToolbarBehavior"]' + ), + getEmptyToolbarBehaviorLabel(resolved.emptyToolbarBehavior) + ); + }); +} + function buildAppearanceSection( - { theme = "inherit", toolbarPosition = "inherit" } = {}, + { + theme = "inherit", + toolbarPosition = "inherit", + alwaysUseDefaultEnvProfile = "inherit", + emptyToolbarBehavior = "inherit" + } = {}, { stateKey } = {} ) { const details = document.createElement("details"); @@ -2434,6 +2681,10 @@ function buildAppearanceSection( themeSelect.value = theme || "inherit"; themeField.appendChild(themeLabel); themeField.appendChild(themeSelect); + const themeHint = document.createElement("div"); + themeHint.className = "hint appearance-inherited hidden"; + themeHint.dataset.appearanceKey = "theme"; + themeField.appendChild(themeHint); const toolbarField = document.createElement("div"); toolbarField.className = "field"; @@ -2466,11 +2717,69 @@ function buildAppearanceSection( toolbarSelect.value = toolbarPosition || "inherit"; toolbarField.appendChild(toolbarLabel); toolbarField.appendChild(toolbarSelect); + const toolbarHint = document.createElement("div"); + toolbarHint.className = "hint appearance-inherited hidden"; + toolbarHint.dataset.appearanceKey = "toolbarPosition"; + toolbarField.appendChild(toolbarHint); + + const defaultField = document.createElement("div"); + defaultField.className = "field"; + const defaultLabel = document.createElement("label"); + defaultLabel.textContent = "Always use default ENV/PROFILE"; + const defaultSelect = document.createElement("select"); + defaultSelect.className = "appearance-default-env-profile"; + const defaultOptions = [ + { value: "inherit", label: "Inherit" }, + { value: "enabled", label: "Enabled" }, + { value: "disabled", label: "Disabled" } + ]; + for (const optValue of defaultOptions) { + const opt = document.createElement("option"); + opt.value = optValue.value; + opt.textContent = optValue.label; + defaultSelect.appendChild(opt); + } + defaultSelect.value = normalizeAppearanceToggle(alwaysUseDefaultEnvProfile); + defaultField.appendChild(defaultLabel); + defaultField.appendChild(defaultSelect); + const defaultHint = document.createElement("div"); + defaultHint.className = "hint appearance-inherited hidden"; + defaultHint.dataset.appearanceKey = "alwaysUseDefaultEnvProfile"; + defaultField.appendChild(defaultHint); + + const emptyToolbarField = document.createElement("div"); + emptyToolbarField.className = "field"; + const emptyToolbarLabel = document.createElement("label"); + emptyToolbarLabel.textContent = "Empty toolbar"; + const emptyToolbarSelect = document.createElement("select"); + emptyToolbarSelect.className = "appearance-empty-toolbar"; + const emptyToolbarOptions = [ + { value: "inherit", label: "Inherit" }, + { value: "hide", label: "Hide Toolbar" }, + { value: "open", label: 'Show "Open SiteCompanion"' } + ]; + for (const optionConfig of emptyToolbarOptions) { + const opt = document.createElement("option"); + opt.value = optionConfig.value; + opt.textContent = optionConfig.label; + emptyToolbarSelect.appendChild(opt); + } + emptyToolbarSelect.value = normalizeEmptyToolbarBehavior( + emptyToolbarBehavior + ); + emptyToolbarField.appendChild(emptyToolbarLabel); + emptyToolbarField.appendChild(emptyToolbarSelect); + const emptyToolbarHint = document.createElement("div"); + emptyToolbarHint.className = "hint appearance-inherited hidden"; + emptyToolbarHint.dataset.appearanceKey = "emptyToolbarBehavior"; + emptyToolbarField.appendChild(emptyToolbarHint); const appearanceRow = document.createElement("div"); - appearanceRow.className = "inline-fields two appearance-fields"; + appearanceRow.className = "inline-fields four appearance-fields"; appearanceRow.appendChild(themeField); appearanceRow.appendChild(toolbarField); + appearanceRow.appendChild(defaultField); + appearanceRow.appendChild(emptyToolbarField); body.appendChild(appearanceRow); details.appendChild(body); registerDetail(details, details.open); @@ -3983,6 +4292,7 @@ function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) { if (confirm(`Delete workspace "${ws.name}"? All items will move to global.`)) { card.remove(); scheduleSidebarErrors(); + updateAppearanceInheritanceIndicators(); updateToc(collectWorkspaces(), collectSites()); } }); @@ -3993,7 +4303,13 @@ function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) { const appearanceSection = buildAppearanceSection( { theme: ws.theme || "inherit", - toolbarPosition: ws.toolbarPosition || "inherit" + toolbarPosition: ws.toolbarPosition || "inherit", + alwaysUseDefaultEnvProfile: normalizeAppearanceToggle( + ws.alwaysUseDefaultEnvProfile + ), + emptyToolbarBehavior: normalizeEmptyToolbarBehavior( + ws.emptyToolbarBehavior + ) }, { stateKey: `workspace:${card.dataset.id}:appearance` } ); @@ -4214,6 +4530,10 @@ function collectSites() { const parsedTarget = parseExtractionTargetInput(extractInput?.value || ""); const themeSelect = card.querySelector(".appearance-theme"); const toolbarSelect = card.querySelector(".appearance-toolbar-position"); + const defaultEnvProfileSelect = card.querySelector( + ".appearance-default-env-profile" + ); + const emptyToolbarSelect = card.querySelector(".appearance-empty-toolbar"); const envsContainer = card.querySelector(".site-envs"); const profilesContainer = card.querySelector(".site-profiles"); const tasksContainer = card.querySelector(".site-tasks"); @@ -4231,6 +4551,12 @@ function collectSites() { extractTarget: parsedTarget.target, theme: themeSelect?.value || "inherit", toolbarPosition: toolbarSelect?.value || "inherit", + alwaysUseDefaultEnvProfile: normalizeAppearanceToggle( + defaultEnvProfileSelect?.value + ), + emptyToolbarBehavior: normalizeEmptyToolbarBehavior( + emptyToolbarSelect?.value + ), envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [], profiles: profilesContainer ? collectProfiles(profilesContainer) : [], tasks: tasksContainer ? collectTasks(tasksContainer) : [], @@ -4380,7 +4706,13 @@ function buildSiteCard(site, allWorkspaces = []) { const appearanceSection = buildAppearanceSection( { theme: site.theme || "inherit", - toolbarPosition: site.toolbarPosition || "inherit" + toolbarPosition: site.toolbarPosition || "inherit", + alwaysUseDefaultEnvProfile: normalizeAppearanceToggle( + site.alwaysUseDefaultEnvProfile + ), + emptyToolbarBehavior: normalizeEmptyToolbarBehavior( + site.emptyToolbarBehavior + ) }, { stateKey: `site:${card.dataset.id}:appearance` } ); @@ -5026,6 +5358,7 @@ function updateShortcutControls(container = shortcutsContainer) { if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; }); scheduleSidebarErrors(); + scheduleTocUpdate(); } function updateTaskControls(container = tasksContainer) { @@ -5039,6 +5372,7 @@ function updateTaskControls(container = tasksContainer) { if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; }); scheduleSidebarErrors(); + scheduleTocUpdate(); } function collectTasks(container = tasksContainer) { @@ -5345,6 +5679,8 @@ async function loadSettings() { toolbarPosition = "bottom-right", toolbarAutoHide: storedToolbarAutoHide = true, alwaysShowOutput: storedAlwaysShowOutput = false, + alwaysUseDefaultEnvProfile: storedAlwaysUseDefaultEnvProfile = false, + emptyToolbarBehavior: storedEmptyToolbarBehavior = "open", sidebarWidth } = await getStorage([ "apiKey", @@ -5367,6 +5703,8 @@ async function loadSettings() { "sites", "toolbarPosition", "toolbarAutoHide", + "emptyToolbarBehavior", + "alwaysUseDefaultEnvProfile", SIDEBAR_WIDTH_KEY ]); @@ -5379,9 +5717,22 @@ async function loadSettings() { if (toolbarAutoHide) { toolbarAutoHide.checked = Boolean(storedToolbarAutoHide); } + if (emptyToolbarBehaviorSelect) { + emptyToolbarBehaviorSelect.value = normalizeEmptyToolbarBehavior( + storedEmptyToolbarBehavior, + false + ); + } if (alwaysShowOutput) { alwaysShowOutput.checked = Boolean(storedAlwaysShowOutput); } + if (alwaysUseDefaultEnvProfileSelect) { + const normalizedDefault = normalizeAppearanceToggle( + storedAlwaysUseDefaultEnvProfile + ); + alwaysUseDefaultEnvProfileSelect.value = + normalizedDefault === "enabled" ? "enabled" : "disabled"; + } if (Number.isFinite(sidebarWidth)) { applySidebarWidth(sidebarWidth); } @@ -5417,6 +5768,12 @@ async function loadSettings() { ...workspace, theme: workspace.theme || "inherit", toolbarPosition: workspace.toolbarPosition || "inherit", + alwaysUseDefaultEnvProfile: normalizeAppearanceToggle( + workspace.alwaysUseDefaultEnvProfile + ), + emptyToolbarBehavior: normalizeEmptyToolbarBehavior( + workspace.emptyToolbarBehavior + ), envConfigs: normalizeConfigList(workspace.envConfigs), profiles: normalizeConfigList(workspace.profiles), tasks: normalizeConfigList(workspace.tasks), @@ -5441,6 +5798,12 @@ async function loadSettings() { extractTarget: normalizedTarget.target, theme: site.theme || "inherit", toolbarPosition: site.toolbarPosition || "inherit", + alwaysUseDefaultEnvProfile: normalizeAppearanceToggle( + site.alwaysUseDefaultEnvProfile + ), + emptyToolbarBehavior: normalizeEmptyToolbarBehavior( + site.emptyToolbarBehavior + ), envConfigs: normalizeConfigList(site.envConfigs), profiles: normalizeConfigList(site.profiles), tasks: normalizeConfigList(site.tasks), @@ -5742,6 +6105,7 @@ async function loadSettings() { updateSidebarErrors(); updateToc(workspaces, sites); renderGlobalSitesList(sites); + updateAppearanceInheritanceIndicators(); } async function saveSettings() { @@ -5870,8 +6234,14 @@ async function saveSettings() { toolbarPosition: toolbarPositionSelect ? toolbarPositionSelect.value : "bottom-right", + emptyToolbarBehavior: emptyToolbarBehaviorSelect + ? emptyToolbarBehaviorSelect.value + : "open", toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true, alwaysShowOutput: alwaysShowOutput ? alwaysShowOutput.checked : false, + alwaysUseDefaultEnvProfile: alwaysUseDefaultEnvProfileSelect + ? alwaysUseDefaultEnvProfileSelect.value === "enabled" + : false, workspaces: updatedWorkspaces, sites: mergedSites }); @@ -6016,6 +6386,8 @@ addWorkspaceBtn.addEventListener("click", () => { name: "New Workspace", theme: "inherit", toolbarPosition: "inherit", + alwaysUseDefaultEnvProfile: "inherit", + emptyToolbarBehavior: "inherit", envConfigs: [], profiles: [], tasks: [], @@ -6032,6 +6404,7 @@ addWorkspaceBtn.addEventListener("click", () => { centerCardInView(newCard); refreshWorkspaceInheritedLists(); scheduleSidebarErrors(); + updateAppearanceInheritanceIndicators(); updateToc(collectWorkspaces(), collectSites()); }); @@ -6044,6 +6417,8 @@ addSiteBtn.addEventListener("click", () => { workspaceId: "global", theme: "inherit", toolbarPosition: "inherit", + alwaysUseDefaultEnvProfile: "inherit", + emptyToolbarBehavior: "inherit", envConfigs: [], profiles: [], tasks: [], @@ -6060,6 +6435,7 @@ addSiteBtn.addEventListener("click", () => { centerCardInView(newCard); refreshSiteInheritedLists(); scheduleSidebarErrors(); + updateAppearanceInheritanceIndicators(); updateToc(collectWorkspaces(), collectSites()); }); @@ -6109,6 +6485,9 @@ async function initSettings() { registerAllDetails(); restoreScrollPosition(); initDirtyObserver(); + if (settingsLayout) { + settingsLayout.addEventListener("input", handleTocNameInput); + } captureSavedSnapshot(); suppressDirtyTracking = false; window.addEventListener("scroll", handleSettingsScroll, { passive: true }); @@ -6563,6 +6942,7 @@ function handleSettingsInputChange() { scheduleSidebarErrors(); scheduleDirtyCheck(); refreshInheritedSourceLabels(); + updateAppearanceInheritanceIndicators(); } document.addEventListener("input", handleSettingsInputChange);