v0.4.8-dev New Release (#3)

# New Features
- Added custom prompt mode
- Always use the default environment and profile for a more compact UI
- Added option to hide the toolbar when it's empty
- Added documentation and icon

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

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-01-20 05:41:07 +00:00
parent 196b659fa1
commit f0db7bb74a
11 changed files with 1203 additions and 72 deletions

239
README.md Normal file
View File

@@ -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.

View File

@@ -17,6 +17,8 @@ const DEFAULT_SETTINGS = {
theme: "system", theme: "system",
toolbarAutoHide: true, toolbarAutoHide: true,
alwaysShowOutput: false, alwaysShowOutput: false,
alwaysUseDefaultEnvProfile: false,
emptyToolbarBehavior: "open",
workspaces: [] workspaces: []
}; };

View File

@@ -360,6 +360,22 @@ function resolveThemeValue(globalTheme, workspace, site) {
return globalTheme || "system"; 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) { function resolveThemeMode(theme) {
if (theme === "dark" || theme === "light") return theme; if (theme === "dark" || theme === "light") return theme;
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { 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"); let toolbar = document.getElementById("sitecompanion-toolbar");
if (toolbar) toolbar.remove(); 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 = document.createElement("div");
toolbar.id = "sitecompanion-toolbar"; toolbar.id = "sitecompanion-toolbar";
@@ -437,7 +463,7 @@ function createToolbar(shortcuts, position = "bottom-right", themeMode = "light"
color: ${tokens.ink}; color: ${tokens.ink};
`; `;
if (options?.unknown) { if (showOpenButton) {
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.type = "button"; btn.type = "button";
btn.textContent = "Open SiteCompanion"; btn.textContent = "Open SiteCompanion";
@@ -457,12 +483,6 @@ function createToolbar(shortcuts, position = "bottom-right", themeMode = "light"
}); });
}); });
toolbar.appendChild(btn); 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 { } else {
for (const shortcut of shortcuts) { for (const shortcut of shortcuts) {
const btn = document.createElement("button"); const btn = document.createElement("button");
@@ -527,7 +547,8 @@ async function refreshToolbar() {
presets = [], presets = [],
toolbarPosition = "bottom-right", toolbarPosition = "bottom-right",
theme = "system", theme = "system",
toolbarAutoHide = true toolbarAutoHide = true,
emptyToolbarBehavior = "open"
} = await chrome.storage.local.get([ } = await chrome.storage.local.get([
"sites", "sites",
"workspaces", "workspaces",
@@ -535,7 +556,8 @@ async function refreshToolbar() {
"presets", "presets",
"toolbarPosition", "toolbarPosition",
"theme", "theme",
"toolbarAutoHide" "toolbarAutoHide",
"emptyToolbarBehavior"
]); ]);
const currentUrl = window.location.href; const currentUrl = window.location.href;
const site = sites.find(s => matchUrl(currentUrl, s.urlPattern)); const site = sites.find(s => matchUrl(currentUrl, s.urlPattern));
@@ -579,7 +601,19 @@ async function refreshToolbar() {
: toolbarPosition; : toolbarPosition;
const resolvedTheme = resolveThemeValue(theme, workspace, site); const resolvedTheme = resolveThemeValue(theme, workspace, site);
const themeMode = resolveThemeMode(resolvedTheme); 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) { } catch (error) {
const message = String(error?.message || ""); const message = String(error?.message || "");
if (message.includes("Extension context invalidated")) { if (message.includes("Extension context invalidated")) {
@@ -596,6 +630,7 @@ async function refreshToolbar() {
let refreshTimer = null; let refreshTimer = null;
let contentChangeTimer = null;
function scheduleToolbarRefresh() { function scheduleToolbarRefresh() {
if (refreshTimer) return; if (refreshTimer) return;
refreshTimer = window.setTimeout(() => { refreshTimer = window.setTimeout(() => {
@@ -610,9 +645,22 @@ function scheduleToolbarRefresh() {
}, 200); }, 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(() => { const observer = new MutationObserver(() => {
if (suppressObserver) return; if (suppressObserver) return;
scheduleToolbarRefresh(); scheduleToolbarRefresh();
scheduleContentChangeNotice();
}); });
observer.observe(document.documentElement, { childList: true, subtree: true }); observer.observe(document.documentElement, { childList: true, subtree: true });

BIN
sitecompanion/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "SiteCompanion", "name": "SiteCompanion",
"version": "0.4.7", "version": "0.4.8",
"description": "AI companion for site-bound text extraction and tasks.", "description": "AI companion for site-bound text extraction and tasks.",
"permissions": ["storage", "activeTab"], "permissions": ["storage", "activeTab"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],
@@ -22,5 +22,8 @@
"options_ui": { "options_ui": {
"page": "settings.html", "page": "settings.html",
"open_in_tab": true "open_in_tab": true
},
"icons": {
"128": "icon128.png"
} }
} }

View File

@@ -47,6 +47,9 @@ body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
color: var(--ink); color: var(--ink);
background: var(--page-bg); background: var(--page-bg);
--control-height: 30px;
--output-max-height-base: 276px;
--output-height-delta: 0px;
} }
.title-block { .title-block {
@@ -119,7 +122,8 @@ label {
select { select {
width: 100%; width: 100%;
padding: 6px 8px; height: var(--control-height);
padding: 0 8px;
border-radius: 10px; border-radius: 10px;
border: 1px solid var(--border); border: 1px solid var(--border);
background: var(--input-bg); background: var(--input-bg);
@@ -138,6 +142,49 @@ select {
margin: 0; 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 { .task-row {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
@@ -145,7 +192,7 @@ select {
} }
.task-row button { .task-row button {
padding: 6px 15px; padding: 0 15px;
} }
.task-row .task-field { .task-row .task-field {
@@ -157,6 +204,37 @@ select {
min-width: 0; 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 { .hidden {
display: none !important; display: none !important;
} }
@@ -221,11 +299,18 @@ button {
font-family: inherit; font-family: inherit;
border: none; border: none;
border-radius: 10px; border-radius: 10px;
padding: 6px 10px; padding: 0 10px;
cursor: pointer; cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease; 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 { button:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
@@ -287,14 +372,14 @@ button:active {
padding: 8px; padding: 8px;
background: var(--output-bg); background: var(--output-bg);
min-height: 210px; min-height: 210px;
max-height: 280px; max-height: calc(var(--output-max-height-base) - var(--output-height-delta));
overflow: hidden; overflow: hidden;
} }
.output-body { .output-body {
margin: 0; margin: 0;
word-break: break-word; word-break: break-word;
max-height: 260px; max-height: calc(var(--output-max-height-base) - var(--output-height-delta) - 20px);
overflow-y: auto; overflow-y: auto;
font-size: 11px; font-size: 11px;
line-height: 1.45; line-height: 1.45;
@@ -384,6 +469,10 @@ button:active {
gap: 8px; gap: 8px;
} }
.footer.compact {
justify-content: flex-end;
}
.footer-left { .footer-left {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -20,8 +20,8 @@
<textarea id="partialTextPaste" rows="4" placeholder="Paste some text here..."></textarea> <textarea id="partialTextPaste" rows="4" placeholder="Paste some text here..."></textarea>
<div id="minimalExtractStatus" class="helper-text hidden"></div> <div id="minimalExtractStatus" class="helper-text hidden"></div>
<div class="row"> <div class="row">
<button id="extractMinimalBtn" class="accent">Try Extracting Minimal</button> <button id="extractMinimalBtn" class="accent control-btn">Try Extracting Minimal</button>
<button id="extractFullBtn" class="ghost">Extract Full Text</button> <button id="extractFullBtn" class="ghost control-btn">Extract Full Text</button>
</div> </div>
</div> </div>
</section> </section>
@@ -43,8 +43,8 @@
<select id="workspaceSelect"></select> <select id="workspaceSelect"></select>
</div> </div>
<div class="row"> <div class="row">
<button id="retryExtractBtn" class="ghost">Retry</button> <button id="retryExtractBtn" class="ghost control-btn">Retry</button>
<button id="confirmSiteBtn" class="accent">Confirm</button> <button id="confirmSiteBtn" class="accent control-btn">Confirm</button>
</div> </div>
</div> </div>
</section> </section>
@@ -65,19 +65,45 @@
<select id="profileSelect"></select> <select id="profileSelect"></select>
</div> </div>
</div> </div>
<div class="task-row"> <div class="env-profile-summary" id="envProfileSummary">
<div class="env-profile-item">
ENV: <span id="envSummaryValue"></span>
</div>
<div class="env-profile-item">
PROFILE: <span id="profileSummaryValue"></span>
</div>
</div>
<div class="task-row" id="normalTaskRow">
<div class="field inline-field task-field"> <div class="field inline-field task-field">
<label for="taskSelect">Task</label> <label for="taskSelect">Task</label>
<select id="taskSelect"></select> <select id="taskSelect"></select>
</div> </div>
<button id="runBtn" class="accent">Run</button> <button id="customTaskBtn" class="ghost control-btn">Custom</button>
<button id="abortBtn" class="ghost stop-btn hidden" disabled>Stop</button> <div id="taskActions">
<button id="runBtn" class="accent control-btn">Run</button>
<button
id="abortBtn"
class="ghost stop-btn hidden control-btn"
disabled
>
Stop
</button>
</div>
</div>
<div class="task-row custom-task-row hidden" id="customTaskRow">
<div class="field custom-task-field">
<textarea id="customTaskInput" rows="2" placeholder="Enter temporary custom task..."></textarea>
</div>
<div class="custom-task-actions">
<button id="normalTaskBtn" class="ghost control-btn">Normal</button>
<div id="taskActionsSlot"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="meta"> <div class="meta">
<span id="postingCount">Site Text: 0 chars</span> <span id="postingCount">Site Text: 0 chars</span>
<span id="promptCount">Task: 0 chars</span> <span id="promptCount">Total: 0 chars</span>
<span id="status" class="status">Idle</span> <span id="status" class="status">Idle</span>
</div> </div>
</section> </section>
@@ -88,9 +114,15 @@
<footer class="footer"> <footer class="footer">
<div class="footer-left"> <div class="footer-left">
<button id="copyRenderedBtn" class="ghost" type="button">Copy</button> <button id="copyRenderedBtn" class="ghost control-btn" type="button">
<button id="copyRawBtn" class="ghost" type="button">Copy Markdown</button> Copy
<button id="clearOutputBtn" class="ghost" type="button">Clear</button> </button>
<button id="copyRawBtn" class="ghost control-btn" type="button">
Copy Markdown
</button>
<button id="clearOutputBtn" class="ghost control-btn" type="button">
Clear
</button>
</div> </div>
<button id="settingsBtn" class="link">Settings</button> <button id="settingsBtn" class="link">Settings</button>
</footer> </footer>

View File

@@ -3,6 +3,16 @@ const abortBtn = document.getElementById("abortBtn");
const taskSelect = document.getElementById("taskSelect"); const taskSelect = document.getElementById("taskSelect");
const envSelect = document.getElementById("envSelect"); const envSelect = document.getElementById("envSelect");
const profileSelect = document.getElementById("profileSelect"); 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 outputEl = document.getElementById("output");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const postingCountEl = document.getElementById("postingCount"); const postingCountEl = document.getElementById("postingCount");
@@ -12,6 +22,8 @@ const copyRenderedBtn = document.getElementById("copyRenderedBtn");
const copyRawBtn = document.getElementById("copyRawBtn"); const copyRawBtn = document.getElementById("copyRawBtn");
const clearOutputBtn = document.getElementById("clearOutputBtn"); const clearOutputBtn = document.getElementById("clearOutputBtn");
const outputSection = document.querySelector(".output"); const outputSection = document.querySelector(".output");
const footerLeft = document.querySelector(".footer-left");
const footer = document.querySelector(".footer");
const OUTPUT_STORAGE_KEY = "lastOutput"; const OUTPUT_STORAGE_KEY = "lastOutput";
const AUTO_RUN_KEY = "autoRunDefaultTask"; const AUTO_RUN_KEY = "autoRunDefaultTask";
@@ -20,6 +32,8 @@ const LAST_TASK_KEY = "lastSelectedTaskId";
const LAST_ENV_KEY = "lastSelectedEnvId"; const LAST_ENV_KEY = "lastSelectedEnvId";
const LAST_PROFILE_KEY = "lastSelectedProfileId"; const LAST_PROFILE_KEY = "lastSelectedProfileId";
const POPUP_DRAFT_KEY = "popupDraft"; const POPUP_DRAFT_KEY = "popupDraft";
const CUSTOM_TASK_MODE_KEY = "customTaskMode";
const CUSTOM_TASK_TEXT_KEY = "customTaskText";
const unknownSiteState = document.getElementById("unknownSiteState"); const unknownSiteState = document.getElementById("unknownSiteState");
const extractionReviewState = document.getElementById("extractionReviewState"); const extractionReviewState = document.getElementById("extractionReviewState");
@@ -57,7 +71,12 @@ const state = {
selectedTaskId: "", selectedTaskId: "",
selectedEnvId: "", selectedEnvId: "",
selectedProfileId: "", selectedProfileId: "",
alwaysShowOutput: false alwaysShowOutput: false,
alwaysUseDefaultEnvProfile: false,
activeTabId: null,
pendingConfigRefresh: false,
customTaskMode: false,
customTaskText: ""
}; };
async function switchState(stateName) { async function switchState(stateName) {
@@ -118,6 +137,7 @@ function applyPopupDraft(draft) {
} else if (typeof draft.siteTextSelector === "string") { } else if (typeof draft.siteTextSelector === "string") {
state.siteTextTarget = { kind: "css", selector: draft.siteTextSelector }; state.siteTextTarget = { kind: "css", selector: draft.siteTextSelector };
} }
updateCounts();
} }
function matchUrl(url, pattern) { function matchUrl(url, pattern) {
@@ -668,6 +688,46 @@ function resolveThemeForPopup(baseTheme) {
return baseTheme || "system"; 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) { function setAnalyzing(isAnalyzing) {
state.isAnalyzing = isAnalyzing; state.isAnalyzing = isAnalyzing;
runBtn.disabled = isAnalyzing; runBtn.disabled = isAnalyzing;
@@ -677,6 +737,10 @@ function setAnalyzing(isAnalyzing) {
updateTaskSelectState(); updateTaskSelectState();
updateEnvSelectState(); updateEnvSelectState();
updateProfileSelectState(); updateProfileSelectState();
if (!isAnalyzing && state.pendingConfigRefresh) {
state.pendingConfigRefresh = false;
scheduleConfigRefresh();
}
} }
function updateOutputVisibility() { function updateOutputVisibility() {
@@ -684,14 +748,175 @@ function updateOutputVisibility() {
const shouldHide = const shouldHide =
state.currentPopupState !== "normal" && !state.alwaysShowOutput; state.currentPopupState !== "normal" && !state.alwaysShowOutput;
outputSection.classList.toggle("hidden", shouldHide); 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() { function updateSiteTextCount() {
postingCountEl.textContent = `Site Text: ${state.siteText.length} chars`; const length = (state.siteText || "").length;
postingCountEl.textContent = `Site Text: ${length} chars`;
} }
function updatePromptCount(count) { 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) { function renderTasks(tasks) {
@@ -726,6 +951,7 @@ function renderEnvironments(envs) {
option.value = ""; option.value = "";
envSelect.appendChild(option); envSelect.appendChild(option);
updateEnvSelectState(); updateEnvSelectState();
updateEnvProfileSummary();
return; return;
} }
@@ -736,6 +962,7 @@ function renderEnvironments(envs) {
envSelect.appendChild(option); envSelect.appendChild(option);
} }
updateEnvSelectState(); updateEnvSelectState();
updateEnvProfileSummary();
} }
function updateTaskSelectState() { function updateTaskSelectState() {
@@ -745,7 +972,10 @@ function updateTaskSelectState() {
function updateEnvSelectState() { function updateEnvSelectState() {
const hasEnvs = state.envs.length > 0; const hasEnvs = state.envs.length > 0;
envSelect.disabled = state.isAnalyzing || !hasEnvs; envSelect.disabled =
state.isAnalyzing ||
!hasEnvs ||
(state.alwaysUseDefaultEnvProfile && !state.customTaskMode);
} }
function renderProfiles(profiles) { function renderProfiles(profiles) {
@@ -758,6 +988,7 @@ function renderProfiles(profiles) {
option.value = ""; option.value = "";
profileSelect.appendChild(option); profileSelect.appendChild(option);
updateProfileSelectState(); updateProfileSelectState();
updateEnvProfileSummary();
return; return;
} }
@@ -768,11 +999,15 @@ function renderProfiles(profiles) {
profileSelect.appendChild(option); profileSelect.appendChild(option);
} }
updateProfileSelectState(); updateProfileSelectState();
updateEnvProfileSummary();
} }
function updateProfileSelectState() { function updateProfileSelectState() {
const hasProfiles = state.profiles.length > 0; const hasProfiles = state.profiles.length > 0;
profileSelect.disabled = state.isAnalyzing || !hasProfiles; profileSelect.disabled =
state.isAnalyzing ||
!hasProfiles ||
(state.alwaysUseDefaultEnvProfile && !state.customTaskMode);
} }
function getTaskDefaultEnvId(task) { function getTaskDefaultEnvId(task) {
@@ -792,6 +1027,8 @@ function setEnvironmentSelection(envId) {
envSelect.value = target; envSelect.value = target;
} }
state.selectedEnvId = target; state.selectedEnvId = target;
updatePromptCount();
updateEnvProfileSummary();
} }
function setProfileSelection(profileId) { function setProfileSelection(profileId) {
@@ -803,6 +1040,8 @@ function setProfileSelection(profileId) {
profileSelect.value = target; profileSelect.value = target;
} }
state.selectedProfileId = target; state.selectedProfileId = target;
updatePromptCount();
updateEnvProfileSummary();
} }
function selectTask(taskId, { resetEnv } = { resetEnv: false }) { function selectTask(taskId, { resetEnv } = { resetEnv: false }) {
@@ -814,6 +1053,7 @@ function selectTask(taskId, { resetEnv } = { resetEnv: false }) {
setEnvironmentSelection(getTaskDefaultEnvId(task)); setEnvironmentSelection(getTaskDefaultEnvId(task));
setProfileSelection(getTaskDefaultProfileId(task)); setProfileSelection(getTaskDefaultProfileId(task));
} }
updatePromptCount();
} }
async function persistSelections() { async function persistSelections() {
@@ -900,6 +1140,7 @@ function ensurePort() {
async function loadConfig() { async function loadConfig() {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const currentUrl = tabs[0]?.url || ""; const currentUrl = tabs[0]?.url || "";
state.activeTabId = tabs[0]?.id || null;
const { lastPopupState, [POPUP_DRAFT_KEY]: popupDraft } = await getStorage([ const { lastPopupState, [POPUP_DRAFT_KEY]: popupDraft } = await getStorage([
"lastPopupState", "lastPopupState",
@@ -926,9 +1167,12 @@ async function loadConfig() {
"sites", "sites",
"theme", "theme",
"alwaysShowOutput", "alwaysShowOutput",
"alwaysUseDefaultEnvProfile",
LAST_TASK_KEY, LAST_TASK_KEY,
LAST_ENV_KEY, LAST_ENV_KEY,
LAST_PROFILE_KEY LAST_PROFILE_KEY,
CUSTOM_TASK_MODE_KEY,
CUSTOM_TASK_TEXT_KEY
]); ]);
const tasks = normalizeConfigList(stored.tasks); const tasks = normalizeConfigList(stored.tasks);
const envs = normalizeConfigList(stored.envConfigs); const envs = normalizeConfigList(stored.envConfigs);
@@ -968,12 +1212,23 @@ async function loadConfig() {
state.currentWorkspace = activeWorkspace; state.currentWorkspace = activeWorkspace;
currentWorkspaceName.textContent = activeWorkspace.name || "Global"; currentWorkspaceName.textContent = activeWorkspace.name || "Global";
} }
if (state.currentSite && !state.siteTextTarget) {
state.siteTextTarget = normalizeStoredExtractTarget(state.currentSite);
}
if (stored.theme) { if (stored.theme) {
state.globalTheme = stored.theme; state.globalTheme = stored.theme;
} }
state.alwaysShowOutput = Boolean(stored.alwaysShowOutput); state.alwaysShowOutput = Boolean(stored.alwaysShowOutput);
state.alwaysUseDefaultEnvProfile = resolveAlwaysUseDefaultEnvProfile(
stored.alwaysUseDefaultEnvProfile,
activeWorkspace,
activeSite
);
applyTheme(resolveThemeForPopup(state.globalTheme)); applyTheme(resolveThemeForPopup(state.globalTheme));
updateOutputVisibility(); updateOutputVisibility();
applyAlwaysUseDefaultEnvProfileState();
state.customTaskMode = Boolean(stored[CUSTOM_TASK_MODE_KEY]);
state.customTaskText = stored[CUSTOM_TASK_TEXT_KEY] || "";
const effectiveEnvs = resolveEffectiveList( const effectiveEnvs = resolveEffectiveList(
envs, envs,
@@ -1000,11 +1255,16 @@ async function loadConfig() {
renderTasks(effectiveTasks); renderTasks(effectiveTasks);
renderEnvironments(effectiveEnvs); renderEnvironments(effectiveEnvs);
renderProfiles(effectiveProfiles); renderProfiles(effectiveProfiles);
if (customTaskInput) {
customTaskInput.value = state.customTaskText;
}
setCustomTaskMode(state.customTaskMode, { persist: false });
if (!effectiveTasks.length) { if (!effectiveTasks.length) {
state.selectedTaskId = ""; state.selectedTaskId = "";
setEnvironmentSelection(effectiveEnvs[0]?.id || ""); setEnvironmentSelection(effectiveEnvs[0]?.id || "");
setProfileSelection(effectiveProfiles[0]?.id || ""); setProfileSelection(effectiveProfiles[0]?.id || "");
updateCounts();
return; return;
} }
@@ -1014,22 +1274,24 @@ async function loadConfig() {
const initialTaskId = effectiveTasks.some((task) => task.id === storedTaskId) const initialTaskId = effectiveTasks.some((task) => task.id === storedTaskId)
? storedTaskId ? storedTaskId
: effectiveTasks[0].id; : effectiveTasks[0].id;
selectTask(initialTaskId, { resetEnv: false }); selectTask(initialTaskId, { resetEnv: state.alwaysUseDefaultEnvProfile });
const task = effectiveTasks.find((item) => item.id === initialTaskId); const task = effectiveTasks.find((item) => item.id === initialTaskId);
if (storedEnvId && effectiveEnvs.some((env) => env.id === storedEnvId)) { if (!state.alwaysUseDefaultEnvProfile) {
setEnvironmentSelection(storedEnvId); if (storedEnvId && effectiveEnvs.some((env) => env.id === storedEnvId)) {
} else { setEnvironmentSelection(storedEnvId);
setEnvironmentSelection(getTaskDefaultEnvId(task)); } else {
} setEnvironmentSelection(getTaskDefaultEnvId(task));
}
if ( if (
storedProfileId && storedProfileId &&
effectiveProfiles.some((profile) => profile.id === storedProfileId) effectiveProfiles.some((profile) => profile.id === storedProfileId)
) { ) {
setProfileSelection(storedProfileId); setProfileSelection(storedProfileId);
} else { } else {
setProfileSelection(getTaskDefaultProfileId(task)); setProfileSelection(getTaskDefaultProfileId(task));
}
} }
if ( if (
@@ -1040,6 +1302,10 @@ async function loadConfig() {
await persistSelections(); await persistSelections();
} }
updateCounts();
if (state.currentSite) {
await refreshSiteContentCounts();
}
maybeRunDefaultTask(); maybeRunDefaultTask();
} }
@@ -1068,8 +1334,7 @@ async function handleExtract() {
state.siteText = response.extracted || ""; state.siteText = response.extracted || "";
state.siteTextTarget = response.target || target; state.siteTextTarget = response.target || target;
updateSiteTextCount(); updateCounts();
updatePromptCount(0);
setStatus("Text extracted."); setStatus("Text extracted.");
return true; return true;
} catch (error) { } catch (error) {
@@ -1094,13 +1359,18 @@ async function handleAnalyze() {
const taskId = taskSelect.value; const taskId = taskSelect.value;
const forcedTask = state.forcedTask; const forcedTask = state.forcedTask;
const task = forcedTask || state.tasks.find((item) => item.id === taskId); const task = forcedTask || state.tasks.find((item) => item.id === taskId);
const useCustomTask = state.customTaskMode && !forcedTask;
if (forcedTask) { if (forcedTask) {
state.forcedTask = null; state.forcedTask = null;
} }
if (!task) { if (!useCustomTask && !task) {
setStatus("Select a task."); setStatus("Select a task.");
return; return;
} }
if (state.alwaysUseDefaultEnvProfile && !forcedTask && !state.customTaskMode) {
setEnvironmentSelection(getTaskDefaultEnvId(task));
setProfileSelection(getTaskDefaultProfileId(task));
}
const { const {
apiKeys = [], apiKeys = [],
@@ -1139,6 +1409,12 @@ async function handleAnalyze() {
} }
const resolvedSystemPrompt = const resolvedSystemPrompt =
activeEnv.systemPrompt ?? systemPrompt ?? ""; 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 = const resolvedApiConfigId =
activeEnv.apiConfigId || activeApiConfigId || resolvedConfigs[0]?.id || ""; activeEnv.apiConfigId || activeApiConfigId || resolvedConfigs[0]?.id || "";
const activeConfig = const activeConfig =
@@ -1194,12 +1470,7 @@ async function handleAnalyze() {
} }
} }
const promptText = buildUserMessage( updatePromptCount();
profileText,
task.text || "",
state.siteText
);
updatePromptCount(promptText.length);
state.outputRaw = ""; state.outputRaw = "";
renderOutput(); renderOutput();
@@ -1220,7 +1491,7 @@ async function handleAnalyze() {
model: resolvedModel, model: resolvedModel,
systemPrompt: resolvedSystemPrompt, systemPrompt: resolvedSystemPrompt,
profileText, profileText,
taskText: task.text || "", taskText: resolvedTaskText,
siteText: state.siteText, siteText: state.siteText,
tabId: tab.id tabId: tab.id
} }
@@ -1298,6 +1569,7 @@ async function runMinimalExtraction(text, minLength = 5) {
state.siteText = response.extracted; state.siteText = response.extracted;
state.siteTextTarget = response.target || { kind: "textScope", text: trimmed }; state.siteTextTarget = response.target || { kind: "textScope", text: trimmed };
extractedPreview.textContent = state.siteText; extractedPreview.textContent = state.siteText;
updateCounts();
await fillSiteDefaultsFromTab(); await fillSiteDefaultsFromTab();
switchState("review"); switchState("review");
await persistPopupDraft(); await persistPopupDraft();
@@ -1336,6 +1608,7 @@ extractFullBtn.addEventListener("click", async () => {
state.siteText = response.extracted; state.siteText = response.extracted;
state.siteTextTarget = target; state.siteTextTarget = target;
extractedPreview.textContent = state.siteText; extractedPreview.textContent = state.siteText;
updateCounts();
await fillSiteDefaultsFromTab(); await fillSiteDefaultsFromTab();
switchState("review"); switchState("review");
await persistPopupDraft(); await persistPopupDraft();
@@ -1372,6 +1645,7 @@ retryExtractBtn.addEventListener("click", () => {
if (workspaceSelect) workspaceSelect.value = "global"; if (workspaceSelect) workspaceSelect.value = "global";
state.siteText = ""; state.siteText = "";
state.siteTextTarget = null; state.siteTextTarget = null;
updateCounts();
setMinimalStatus(""); setMinimalStatus("");
void clearPopupDraft(); void clearPopupDraft();
setStatus("Ready."); setStatus("Ready.");
@@ -1427,10 +1701,24 @@ confirmSiteBtn.addEventListener("click", async () => {
currentWorkspaceName.textContent = state.currentWorkspace.name || "Global"; currentWorkspaceName.textContent = state.currentWorkspace.name || "Global";
await loadConfig(); await loadConfig();
await switchState("normal"); await switchState("normal");
updateSiteTextCount(); updateCounts();
setStatus("Site saved."); 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); runBtn.addEventListener("click", handleExtractAndAnalyze);
abortBtn.addEventListener("click", handleAbort); abortBtn.addEventListener("click", handleAbort);
settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage()); settingsBtn.addEventListener("click", () => chrome.runtime.openOptionsPage());
@@ -1450,8 +1738,7 @@ profileSelect.addEventListener("change", () => {
void persistSelections(); void persistSelections();
}); });
updateSiteTextCount(); updateCounts();
updatePromptCount(0);
renderOutput(); renderOutput();
setAnalyzing(false); setAnalyzing(false);
void loadTheme(); void loadTheme();
@@ -1475,6 +1762,7 @@ async function loadShortcutRunRequest() {
state.shortcutRunPending = true; state.shortcutRunPending = true;
await chrome.storage.local.remove(SHORTCUT_RUN_KEY); await chrome.storage.local.remove(SHORTCUT_RUN_KEY);
setCustomTaskMode(false);
if (!state.tasks.length) { if (!state.tasks.length) {
await loadConfig(); await loadConfig();
@@ -1538,7 +1826,13 @@ async function loadShortcutRunRequest() {
await persistSelections(); await persistSelections();
state.autoRunPending = false; state.autoRunPending = false;
state.shortcutRunPending = 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() { async function loadAutoRunRequest() {
@@ -1597,4 +1891,28 @@ chrome.storage.onChanged.addListener((changes) => {
state.alwaysShowOutput = Boolean(changes.alwaysShowOutput.newValue); state.alwaysShowOutput = Boolean(changes.alwaysShowOutput.newValue);
updateOutputVisibility(); 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();
}); });

View File

@@ -749,6 +749,10 @@ button:active {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.inline-fields.four {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.inline-fields .field { .inline-fields .field {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@@ -103,7 +103,7 @@
</div> </div>
</summary> </summary>
<div class="panel-body"> <div class="panel-body">
<div class="inline-fields two appearance-fields"> <div class="inline-fields four appearance-fields">
<div class="field"> <div class="field">
<label for="themeSelect">Theme</label> <label for="themeSelect">Theme</label>
<select id="themeSelect"> <select id="themeSelect">
@@ -122,6 +122,22 @@
<option value="bottom-center">Bottom Center</option> <option value="bottom-center">Bottom Center</option>
</select> </select>
</div> </div>
<div class="field">
<label for="alwaysUseDefaultEnvProfileSelect">
Always use default ENV/PROFILE
</label>
<select id="alwaysUseDefaultEnvProfileSelect">
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
</select>
</div>
<div class="field">
<label for="emptyToolbarBehaviorSelect">Empty toolbar</label>
<select id="emptyToolbarBehaviorSelect">
<option value="hide">Hide Toolbar</option>
<option value="open">Show "Open SiteCompanion"</option>
</select>
</div>
</div> </div>
<div class="inline-fields two appearance-toggles"> <div class="inline-fields two appearance-toggles">
<div class="field"> <div class="field">

View File

@@ -19,8 +19,14 @@ const statusSidebarEl = document.getElementById("statusSidebar");
const sidebarErrorsEl = document.getElementById("sidebarErrors"); const sidebarErrorsEl = document.getElementById("sidebarErrors");
const themeSelect = document.getElementById("themeSelect"); const themeSelect = document.getElementById("themeSelect");
const toolbarPositionSelect = document.getElementById("toolbarPositionSelect"); const toolbarPositionSelect = document.getElementById("toolbarPositionSelect");
const emptyToolbarBehaviorSelect = document.getElementById(
"emptyToolbarBehaviorSelect"
);
const toolbarAutoHide = document.getElementById("toolbarAutoHide"); const toolbarAutoHide = document.getElementById("toolbarAutoHide");
const alwaysShowOutput = document.getElementById("alwaysShowOutput"); const alwaysShowOutput = document.getElementById("alwaysShowOutput");
const alwaysUseDefaultEnvProfileSelect = document.getElementById(
"alwaysUseDefaultEnvProfileSelect"
);
const globalSitesContainer = document.getElementById("globalSites"); const globalSitesContainer = document.getElementById("globalSites");
const toc = document.querySelector(".toc"); const toc = document.querySelector(".toc");
const tocResizer = document.getElementById("tocResizer"); const tocResizer = document.getElementById("tocResizer");
@@ -453,8 +459,14 @@ function buildSettingsSnapshot() {
toolbarPosition: toolbarPositionSelect toolbarPosition: toolbarPositionSelect
? toolbarPositionSelect.value ? toolbarPositionSelect.value
: "bottom-right", : "bottom-right",
emptyToolbarBehavior: emptyToolbarBehaviorSelect
? emptyToolbarBehaviorSelect.value
: "open",
toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true, 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) { function renderGlobalSitesList(sites) {
if (!globalSitesContainer) return; if (!globalSitesContainer) return;
globalSitesContainer.innerHTML = ""; globalSitesContainer.innerHTML = "";
@@ -1230,6 +1271,7 @@ function updateApiConfigControls() {
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors(); scheduleSidebarErrors();
scheduleTocUpdate();
} }
function buildApiKeyCard(entry) { function buildApiKeyCard(entry) {
@@ -1404,6 +1446,7 @@ function updateApiKeyControls() {
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors(); scheduleSidebarErrors();
scheduleTocUpdate();
} }
function updateApiConfigKeyOptions() { function updateApiConfigKeyOptions() {
@@ -1630,6 +1673,7 @@ function updateEnvControls(container = envConfigsContainer) {
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors(); scheduleSidebarErrors();
scheduleTocUpdate();
} }
function updateTaskEnvOptionsForContainer(container, envs, allEnvsById) { function updateTaskEnvOptionsForContainer(container, envs, allEnvsById) {
@@ -1879,6 +1923,7 @@ function updateProfileControls(container = profilesContainer) {
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors(); scheduleSidebarErrors();
scheduleTocUpdate();
} }
function updateTaskProfileOptionsForContainer(container, profiles, allProfilesById) { function updateTaskProfileOptionsForContainer(container, profiles, allProfilesById) {
@@ -1952,7 +1997,9 @@ function updateEnvApiOptionsForContainer(container, apiConfigs) {
if (!container) return; if (!container) return;
const selects = container.querySelectorAll(".env-config-api-select"); const selects = container.querySelectorAll(".env-config-api-select");
selects.forEach((select) => { selects.forEach((select) => {
select.dataset.preferred = select.value; if (select.value) {
select.dataset.preferred = select.value;
}
populateSelect(select, apiConfigs, "No API configs configured"); populateSelect(select, apiConfigs, "No API configs configured");
}); });
} }
@@ -2195,15 +2242,21 @@ function updateShortcutOptionsForContainer(container, options = {}) {
const profileSelect = card.querySelector(".shortcut-profile"); const profileSelect = card.querySelector(".shortcut-profile");
const taskSelect = card.querySelector(".shortcut-task"); const taskSelect = card.querySelector(".shortcut-task");
if (envSelect) { if (envSelect) {
envSelect.dataset.preferred = envSelect.value; if (envSelect.value) {
envSelect.dataset.preferred = envSelect.value;
}
populateSelect(envSelect, envs, "No environments configured"); populateSelect(envSelect, envs, "No environments configured");
} }
if (profileSelect) { if (profileSelect) {
profileSelect.dataset.preferred = profileSelect.value; if (profileSelect.value) {
profileSelect.dataset.preferred = profileSelect.value;
}
populateSelect(profileSelect, profiles, "No profiles configured"); populateSelect(profileSelect, profiles, "No profiles configured");
} }
if (taskSelect) { if (taskSelect) {
taskSelect.dataset.preferred = taskSelect.value; if (taskSelect.value) {
taskSelect.dataset.preferred = taskSelect.value;
}
populateSelect(taskSelect, tasks, "No tasks configured"); populateSelect(taskSelect, tasks, "No tasks configured");
} }
}); });
@@ -2246,6 +2299,10 @@ function collectWorkspaces() {
const nameInput = card.querySelector(".workspace-name"); const nameInput = card.querySelector(".workspace-name");
const themeSelect = card.querySelector(".appearance-theme"); const themeSelect = card.querySelector(".appearance-theme");
const toolbarSelect = card.querySelector(".appearance-toolbar-position"); 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 // Collect nested resources
const envsContainer = card.querySelector(".workspace-envs"); const envsContainer = card.querySelector(".workspace-envs");
@@ -2269,6 +2326,12 @@ function collectWorkspaces() {
name: (nameInput?.value || "Untitled Workspace").trim(), name: (nameInput?.value || "Untitled Workspace").trim(),
theme: themeSelect?.value || "inherit", theme: themeSelect?.value || "inherit",
toolbarPosition: toolbarSelect?.value || "inherit", toolbarPosition: toolbarSelect?.value || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
defaultEnvProfileSelect?.value
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
emptyToolbarSelect?.value
),
envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [], envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [],
profiles: profilesContainer ? collectProfiles(profilesContainer) : [], profiles: profilesContainer ? collectProfiles(profilesContainer) : [],
tasks: tasksContainer ? collectTasks(tasksContainer) : [], tasks: tasksContainer ? collectTasks(tasksContainer) : [],
@@ -2385,8 +2448,192 @@ function renderWorkspaceSection(title, containerClass, items, builder, newItemFa
return details; 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( function buildAppearanceSection(
{ theme = "inherit", toolbarPosition = "inherit" } = {}, {
theme = "inherit",
toolbarPosition = "inherit",
alwaysUseDefaultEnvProfile = "inherit",
emptyToolbarBehavior = "inherit"
} = {},
{ stateKey } = {} { stateKey } = {}
) { ) {
const details = document.createElement("details"); const details = document.createElement("details");
@@ -2434,6 +2681,10 @@ function buildAppearanceSection(
themeSelect.value = theme || "inherit"; themeSelect.value = theme || "inherit";
themeField.appendChild(themeLabel); themeField.appendChild(themeLabel);
themeField.appendChild(themeSelect); 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"); const toolbarField = document.createElement("div");
toolbarField.className = "field"; toolbarField.className = "field";
@@ -2466,11 +2717,69 @@ function buildAppearanceSection(
toolbarSelect.value = toolbarPosition || "inherit"; toolbarSelect.value = toolbarPosition || "inherit";
toolbarField.appendChild(toolbarLabel); toolbarField.appendChild(toolbarLabel);
toolbarField.appendChild(toolbarSelect); 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"); const appearanceRow = document.createElement("div");
appearanceRow.className = "inline-fields two appearance-fields"; appearanceRow.className = "inline-fields four appearance-fields";
appearanceRow.appendChild(themeField); appearanceRow.appendChild(themeField);
appearanceRow.appendChild(toolbarField); appearanceRow.appendChild(toolbarField);
appearanceRow.appendChild(defaultField);
appearanceRow.appendChild(emptyToolbarField);
body.appendChild(appearanceRow); body.appendChild(appearanceRow);
details.appendChild(body); details.appendChild(body);
registerDetail(details, details.open); 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.`)) { if (confirm(`Delete workspace "${ws.name}"? All items will move to global.`)) {
card.remove(); card.remove();
scheduleSidebarErrors(); scheduleSidebarErrors();
updateAppearanceInheritanceIndicators();
updateToc(collectWorkspaces(), collectSites()); updateToc(collectWorkspaces(), collectSites());
} }
}); });
@@ -3993,7 +4303,13 @@ function buildWorkspaceCard(ws, allWorkspaces = [], allSites = []) {
const appearanceSection = buildAppearanceSection( const appearanceSection = buildAppearanceSection(
{ {
theme: ws.theme || "inherit", 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` } { stateKey: `workspace:${card.dataset.id}:appearance` }
); );
@@ -4214,6 +4530,10 @@ function collectSites() {
const parsedTarget = parseExtractionTargetInput(extractInput?.value || ""); const parsedTarget = parseExtractionTargetInput(extractInput?.value || "");
const themeSelect = card.querySelector(".appearance-theme"); const themeSelect = card.querySelector(".appearance-theme");
const toolbarSelect = card.querySelector(".appearance-toolbar-position"); 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 envsContainer = card.querySelector(".site-envs");
const profilesContainer = card.querySelector(".site-profiles"); const profilesContainer = card.querySelector(".site-profiles");
const tasksContainer = card.querySelector(".site-tasks"); const tasksContainer = card.querySelector(".site-tasks");
@@ -4231,6 +4551,12 @@ function collectSites() {
extractTarget: parsedTarget.target, extractTarget: parsedTarget.target,
theme: themeSelect?.value || "inherit", theme: themeSelect?.value || "inherit",
toolbarPosition: toolbarSelect?.value || "inherit", toolbarPosition: toolbarSelect?.value || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
defaultEnvProfileSelect?.value
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
emptyToolbarSelect?.value
),
envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [], envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [],
profiles: profilesContainer ? collectProfiles(profilesContainer) : [], profiles: profilesContainer ? collectProfiles(profilesContainer) : [],
tasks: tasksContainer ? collectTasks(tasksContainer) : [], tasks: tasksContainer ? collectTasks(tasksContainer) : [],
@@ -4380,7 +4706,13 @@ function buildSiteCard(site, allWorkspaces = []) {
const appearanceSection = buildAppearanceSection( const appearanceSection = buildAppearanceSection(
{ {
theme: site.theme || "inherit", 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` } { stateKey: `site:${card.dataset.id}:appearance` }
); );
@@ -5026,6 +5358,7 @@ function updateShortcutControls(container = shortcutsContainer) {
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors(); scheduleSidebarErrors();
scheduleTocUpdate();
} }
function updateTaskControls(container = tasksContainer) { function updateTaskControls(container = tasksContainer) {
@@ -5039,6 +5372,7 @@ function updateTaskControls(container = tasksContainer) {
if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1; if (moveDownBtn) moveDownBtn.disabled = index === cards.length - 1;
}); });
scheduleSidebarErrors(); scheduleSidebarErrors();
scheduleTocUpdate();
} }
function collectTasks(container = tasksContainer) { function collectTasks(container = tasksContainer) {
@@ -5345,6 +5679,8 @@ async function loadSettings() {
toolbarPosition = "bottom-right", toolbarPosition = "bottom-right",
toolbarAutoHide: storedToolbarAutoHide = true, toolbarAutoHide: storedToolbarAutoHide = true,
alwaysShowOutput: storedAlwaysShowOutput = false, alwaysShowOutput: storedAlwaysShowOutput = false,
alwaysUseDefaultEnvProfile: storedAlwaysUseDefaultEnvProfile = false,
emptyToolbarBehavior: storedEmptyToolbarBehavior = "open",
sidebarWidth sidebarWidth
} = await getStorage([ } = await getStorage([
"apiKey", "apiKey",
@@ -5367,6 +5703,8 @@ async function loadSettings() {
"sites", "sites",
"toolbarPosition", "toolbarPosition",
"toolbarAutoHide", "toolbarAutoHide",
"emptyToolbarBehavior",
"alwaysUseDefaultEnvProfile",
SIDEBAR_WIDTH_KEY SIDEBAR_WIDTH_KEY
]); ]);
@@ -5379,9 +5717,22 @@ async function loadSettings() {
if (toolbarAutoHide) { if (toolbarAutoHide) {
toolbarAutoHide.checked = Boolean(storedToolbarAutoHide); toolbarAutoHide.checked = Boolean(storedToolbarAutoHide);
} }
if (emptyToolbarBehaviorSelect) {
emptyToolbarBehaviorSelect.value = normalizeEmptyToolbarBehavior(
storedEmptyToolbarBehavior,
false
);
}
if (alwaysShowOutput) { if (alwaysShowOutput) {
alwaysShowOutput.checked = Boolean(storedAlwaysShowOutput); alwaysShowOutput.checked = Boolean(storedAlwaysShowOutput);
} }
if (alwaysUseDefaultEnvProfileSelect) {
const normalizedDefault = normalizeAppearanceToggle(
storedAlwaysUseDefaultEnvProfile
);
alwaysUseDefaultEnvProfileSelect.value =
normalizedDefault === "enabled" ? "enabled" : "disabled";
}
if (Number.isFinite(sidebarWidth)) { if (Number.isFinite(sidebarWidth)) {
applySidebarWidth(sidebarWidth); applySidebarWidth(sidebarWidth);
} }
@@ -5417,6 +5768,12 @@ async function loadSettings() {
...workspace, ...workspace,
theme: workspace.theme || "inherit", theme: workspace.theme || "inherit",
toolbarPosition: workspace.toolbarPosition || "inherit", toolbarPosition: workspace.toolbarPosition || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
workspace.alwaysUseDefaultEnvProfile
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
workspace.emptyToolbarBehavior
),
envConfigs: normalizeConfigList(workspace.envConfigs), envConfigs: normalizeConfigList(workspace.envConfigs),
profiles: normalizeConfigList(workspace.profiles), profiles: normalizeConfigList(workspace.profiles),
tasks: normalizeConfigList(workspace.tasks), tasks: normalizeConfigList(workspace.tasks),
@@ -5441,6 +5798,12 @@ async function loadSettings() {
extractTarget: normalizedTarget.target, extractTarget: normalizedTarget.target,
theme: site.theme || "inherit", theme: site.theme || "inherit",
toolbarPosition: site.toolbarPosition || "inherit", toolbarPosition: site.toolbarPosition || "inherit",
alwaysUseDefaultEnvProfile: normalizeAppearanceToggle(
site.alwaysUseDefaultEnvProfile
),
emptyToolbarBehavior: normalizeEmptyToolbarBehavior(
site.emptyToolbarBehavior
),
envConfigs: normalizeConfigList(site.envConfigs), envConfigs: normalizeConfigList(site.envConfigs),
profiles: normalizeConfigList(site.profiles), profiles: normalizeConfigList(site.profiles),
tasks: normalizeConfigList(site.tasks), tasks: normalizeConfigList(site.tasks),
@@ -5742,6 +6105,7 @@ async function loadSettings() {
updateSidebarErrors(); updateSidebarErrors();
updateToc(workspaces, sites); updateToc(workspaces, sites);
renderGlobalSitesList(sites); renderGlobalSitesList(sites);
updateAppearanceInheritanceIndicators();
} }
async function saveSettings() { async function saveSettings() {
@@ -5870,8 +6234,14 @@ async function saveSettings() {
toolbarPosition: toolbarPositionSelect toolbarPosition: toolbarPositionSelect
? toolbarPositionSelect.value ? toolbarPositionSelect.value
: "bottom-right", : "bottom-right",
emptyToolbarBehavior: emptyToolbarBehaviorSelect
? emptyToolbarBehaviorSelect.value
: "open",
toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true, toolbarAutoHide: toolbarAutoHide ? toolbarAutoHide.checked : true,
alwaysShowOutput: alwaysShowOutput ? alwaysShowOutput.checked : false, alwaysShowOutput: alwaysShowOutput ? alwaysShowOutput.checked : false,
alwaysUseDefaultEnvProfile: alwaysUseDefaultEnvProfileSelect
? alwaysUseDefaultEnvProfileSelect.value === "enabled"
: false,
workspaces: updatedWorkspaces, workspaces: updatedWorkspaces,
sites: mergedSites sites: mergedSites
}); });
@@ -6016,6 +6386,8 @@ addWorkspaceBtn.addEventListener("click", () => {
name: "New Workspace", name: "New Workspace",
theme: "inherit", theme: "inherit",
toolbarPosition: "inherit", toolbarPosition: "inherit",
alwaysUseDefaultEnvProfile: "inherit",
emptyToolbarBehavior: "inherit",
envConfigs: [], envConfigs: [],
profiles: [], profiles: [],
tasks: [], tasks: [],
@@ -6032,6 +6404,7 @@ addWorkspaceBtn.addEventListener("click", () => {
centerCardInView(newCard); centerCardInView(newCard);
refreshWorkspaceInheritedLists(); refreshWorkspaceInheritedLists();
scheduleSidebarErrors(); scheduleSidebarErrors();
updateAppearanceInheritanceIndicators();
updateToc(collectWorkspaces(), collectSites()); updateToc(collectWorkspaces(), collectSites());
}); });
@@ -6044,6 +6417,8 @@ addSiteBtn.addEventListener("click", () => {
workspaceId: "global", workspaceId: "global",
theme: "inherit", theme: "inherit",
toolbarPosition: "inherit", toolbarPosition: "inherit",
alwaysUseDefaultEnvProfile: "inherit",
emptyToolbarBehavior: "inherit",
envConfigs: [], envConfigs: [],
profiles: [], profiles: [],
tasks: [], tasks: [],
@@ -6060,6 +6435,7 @@ addSiteBtn.addEventListener("click", () => {
centerCardInView(newCard); centerCardInView(newCard);
refreshSiteInheritedLists(); refreshSiteInheritedLists();
scheduleSidebarErrors(); scheduleSidebarErrors();
updateAppearanceInheritanceIndicators();
updateToc(collectWorkspaces(), collectSites()); updateToc(collectWorkspaces(), collectSites());
}); });
@@ -6109,6 +6485,9 @@ async function initSettings() {
registerAllDetails(); registerAllDetails();
restoreScrollPosition(); restoreScrollPosition();
initDirtyObserver(); initDirtyObserver();
if (settingsLayout) {
settingsLayout.addEventListener("input", handleTocNameInput);
}
captureSavedSnapshot(); captureSavedSnapshot();
suppressDirtyTracking = false; suppressDirtyTracking = false;
window.addEventListener("scroll", handleSettingsScroll, { passive: true }); window.addEventListener("scroll", handleSettingsScroll, { passive: true });
@@ -6563,6 +6942,7 @@ function handleSettingsInputChange() {
scheduleSidebarErrors(); scheduleSidebarErrors();
scheduleDirtyCheck(); scheduleDirtyCheck();
refreshInheritedSourceLabels(); refreshInheritedSourceLabels();
updateAppearanceInheritanceIndicators();
} }
document.addEventListener("input", handleSettingsInputChange); document.addEventListener("input", handleSettingsInputChange);