improve(ui): remove the Context Profile presets card from Quick Settings (#102748)

* fix(control-ui): remove the Context Profile presets card from Quick Settings

* fix(control-ui): regenerate i18n raw-copy baseline after Gateway Host card rebase
This commit is contained in:
Peter Steinberger
2026-07-09 12:50:50 +01:00
committed by GitHub
parent 1fdb0e79d3
commit 8d6c54f513
9 changed files with 120 additions and 748 deletions
+29 -92
View File
@@ -2472,62 +2472,6 @@
"path": "ui/src/pages/config/mcp.ts",
"text": "tool filter"
},
{
"count": 1,
"kind": "object-property",
"name": "description",
"path": "ui/src/pages/config/presets.ts",
"text": "Balanced default for daily use."
},
{
"count": 1,
"kind": "object-property",
"name": "description",
"path": "ui/src/pages/config/presets.ts",
"text": "Highest context budget for repo work."
},
{
"count": 1,
"kind": "object-property",
"name": "description",
"path": "ui/src/pages/config/presets.ts",
"text": "Lean follow-ups for shared bots."
},
{
"count": 1,
"kind": "object-property",
"name": "description",
"path": "ui/src/pages/config/presets.ts",
"text": "Smallest context budget and lowest cost."
},
{
"count": 1,
"kind": "object-property",
"name": "label",
"path": "ui/src/pages/config/presets.ts",
"text": "Code Agent"
},
{
"count": 1,
"kind": "object-property",
"name": "label",
"path": "ui/src/pages/config/presets.ts",
"text": "Minimal"
},
{
"count": 1,
"kind": "object-property",
"name": "label",
"path": "ui/src/pages/config/presets.ts",
"text": "Personal Assistant"
},
{
"count": 1,
"kind": "object-property",
"name": "label",
"path": "ui/src/pages/config/presets.ts",
"text": "Team Bot"
},
{
"count": 1,
"kind": "html-attribute",
@@ -2570,13 +2514,6 @@
"path": "ui/src/pages/config/quick.ts",
"text": "Browse →"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/pages/config/quick.ts",
"text": "Choose how much workspace context OpenClaw injects into each run. Profiles only change bootstrap size and follow-up reinjection — never your model, tools, channels, or theme."
},
{
"count": 1,
"kind": "html-text",
@@ -2612,20 +2549,6 @@
"path": "ui/src/pages/config/quick.ts",
"text": "Connect →"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/pages/config/quick.ts",
"text": "Current"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/pages/config/quick.ts",
"text": "Custom"
},
{
"count": 1,
"kind": "html-text",
@@ -2694,21 +2617,7 @@
"kind": "html-text",
"name": "text",
"path": "ui/src/pages/config/quick.ts",
"text": "Pending"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/pages/config/quick.ts",
"text": "Saved"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/pages/config/quick.ts",
"text": "Selected"
"text": "Save stores your changes. Apply Now also reloads the current session."
},
{
"count": 1,
@@ -2745,6 +2654,13 @@
"path": "ui/src/pages/config/quick.ts",
"text": "Thinking"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/pages/config/quick.ts",
"text": "Unsaved changes"
},
{
"count": 1,
"kind": "html-text",
@@ -2759,6 +2675,13 @@
"path": "ui/src/pages/config/quick.ts",
"text": "Claw"
},
{
"count": 3,
"kind": "object-property",
"name": "label",
"path": "ui/src/pages/config/quick.ts",
"text": "CPU"
},
{
"count": 1,
"kind": "object-property",
@@ -2766,6 +2689,13 @@
"path": "ui/src/pages/config/quick.ts",
"text": "Dash"
},
{
"count": 2,
"kind": "object-property",
"name": "label",
"path": "ui/src/pages/config/quick.ts",
"text": "Disk"
},
{
"count": 1,
"kind": "object-property",
@@ -2787,6 +2717,13 @@
"path": "ui/src/pages/config/quick.ts",
"text": "M"
},
{
"count": 2,
"kind": "object-property",
"name": "label",
"path": "ui/src/pages/config/quick.ts",
"text": "Memory"
},
{
"count": 1,
"kind": "object-property",
-92
View File
@@ -10,7 +10,6 @@ import {
resetConfigPendingChanges,
saveConfig,
stageDefaultAgentConfigEntry,
stageConfigPreset,
updateMcpServerEnabled,
updateConfigFormValue,
updateConfigRawValue,
@@ -514,97 +513,6 @@ describe("updateMcpServerEnabled", () => {
});
});
describe("stageConfigPreset", () => {
it("ignores preset staging before a config snapshot is ready", () => {
const state = createState();
stageConfigPreset(state, {
agents: {
defaults: {
bootstrapMaxChars: 50_000,
bootstrapTotalMaxChars: 300_000,
contextInjection: "always",
},
},
});
expect(state.configForm).toBeNull();
expect(state.configRaw).toBe("");
expect(state.configFormDirty).toBe(false);
});
it("stages preset changes without dropping unrelated config", () => {
const state = createState();
applyConfigSnapshot(state, {
config: {
agents: {
defaults: {
bootstrapMaxChars: 12_000,
bootstrapTotalMaxChars: 60_000,
contextInjection: "always",
},
},
gateway: { mode: "local" },
},
valid: true,
issues: [],
raw: '{\n "agents": {\n "defaults": {\n "bootstrapMaxChars": 12000,\n "bootstrapTotalMaxChars": 60000,\n "contextInjection": "always"\n }\n },\n "gateway": {\n "mode": "local"\n }\n}\n',
});
stageConfigPreset(state, {
agents: {
defaults: {
bootstrapMaxChars: 50_000,
bootstrapTotalMaxChars: 300_000,
contextInjection: "always",
},
},
});
expect(state.configFormDirty).toBe(true);
expect(state.configForm).toEqual({
agents: {
defaults: {
bootstrapMaxChars: 50_000,
bootstrapTotalMaxChars: 300_000,
contextInjection: "always",
},
},
gateway: { mode: "local" },
});
});
it("stays clean when the staged preset already matches the saved config", () => {
const state = createState();
applyConfigSnapshot(state, {
config: {
agents: {
defaults: {
bootstrapMaxChars: 20_000,
bootstrapTotalMaxChars: 150_000,
contextInjection: "always",
},
},
},
valid: true,
issues: [],
raw: '{\n "agents": {\n "defaults": {\n "bootstrapMaxChars": 20000,\n "bootstrapTotalMaxChars": 150000,\n "contextInjection": "always"\n }\n }\n}\n',
});
stageConfigPreset(state, {
agents: {
defaults: {
bootstrapMaxChars: 20_000,
bootstrapTotalMaxChars: 150_000,
contextInjection: "always",
},
},
});
expect(state.configFormDirty).toBe(false);
});
});
describe("resetConfigPendingChanges", () => {
it("restores the original form and raw config snapshot", () => {
const state = createState();
-17
View File
@@ -1,5 +1,4 @@
// Control UI runtime config capability and shared config-domain mutations.
import { applyMergePatch } from "../../../../src/config/merge-patch.ts";
import type { GatewayBrowserClient } from "../../api/gateway.ts";
import type { ConfigSchemaResponse, ConfigSnapshot, ConfigUiHints } from "../../api/types.ts";
import { schemaType, type JsonSchema } from "../../components/config-form.shared.ts";
@@ -63,7 +62,6 @@ export type RuntimeConfigCapability = {
removeFormValue: (path: Array<string | number>) => void;
setRaw: (value: string) => void;
resetDraft: () => void;
stagePreset: (patch: Record<string, unknown>) => void;
save: () => Promise<boolean>;
apply: () => Promise<boolean>;
openFile: () => Promise<void>;
@@ -634,20 +632,6 @@ export function updateConfigRawValue(state: ConfigState, value: string) {
}
}
export function stageConfigPreset(state: ConfigState, patch: Record<string, unknown>) {
const snapshotConfig = resolveEditableSnapshotConfig(state.configSnapshot);
const baseSource = state.configForm ?? snapshotConfig;
if (!baseSource || (!state.configForm && !state.configSnapshot?.hash)) {
return;
}
const base = cloneConfigObject(baseSource);
const merged = applyMergePatch(base, patch);
if (!merged || typeof merged !== "object" || Array.isArray(merged)) {
return;
}
syncConfigDraft(state, cloneConfigObject(merged as Record<string, unknown>));
}
export function resetConfigPendingChanges(state: ConfigState) {
const editableConfig = resolveEditableSnapshotConfig(state.configSnapshot);
state.configForm = cloneConfigObject(state.configFormOriginal ?? editableConfig ?? {});
@@ -872,7 +856,6 @@ export function createRuntimeConfigCapability(
removeFormValue: (path) => mutate(() => removeConfigFormValue(state, path)),
setRaw: (value) => mutate(() => updateConfigRawValue(state, value)),
resetDraft: () => mutate(() => resetConfigPendingChanges(state)),
stagePreset: (patch) => mutate(() => stageConfigPreset(state, patch)),
save: () => run(() => saveConfig(state)),
apply: () => run(() => applyConfig(state)),
openFile: () => run(() => openConfigFile(state)),
-12
View File
@@ -24,7 +24,6 @@ import { renderSettingsWorkspace } from "../../components/settings-workspace.ts"
import { t } from "../../i18n/index.ts";
import { isMissingOperatorReadScopeError } from "../../lib/gateway-errors.ts";
import { renderMcp } from "./mcp.ts";
import { getPresetById } from "./presets.ts";
import {
renderQuickSettings,
type QuickSettingsChannel,
@@ -799,21 +798,10 @@ export class ConfigPage extends LitElement {
assistantName: appConfig.assistantIdentity.name,
version:
appConfig.serverVersion ?? this.context.gateway.snapshot.hello?.server?.version ?? "",
configObject,
savedConfigObject:
asConfigRecord(
runtimeConfig.state.configFormOriginal ?? runtimeConfig.state.configSnapshot?.config,
) ?? {},
configDirty: runtimeConfig.state.configFormDirty,
configSaving: runtimeConfig.state.configSaving,
configApplying: runtimeConfig.state.configApplying,
configReady: Boolean(runtimeConfig.state.configSnapshot?.hash),
onSelectPreset: (id) => {
const preset = getPresetById(id);
if (preset) {
runtimeConfig.stagePreset(preset.patch);
}
},
onResetConfig: () => runtimeConfig.resetDraft(),
onSaveConfig: () => void runtimeConfig.save(),
onApplyConfig: () => void runtimeConfig.apply(),
-84
View File
@@ -1,84 +0,0 @@
// Control UI tests cover config presets behavior.
import { describe, expect, it } from "vitest";
import { OpenClawSchema } from "../../../../src/config/zod-schema.js";
import { CONFIG_PRESETS, detectActivePreset } from "./presets.ts";
describe("detectActivePreset", () => {
it("keeps every preset patch valid for the runtime config schema", () => {
expect(
CONFIG_PRESETS.map((preset) => ({
id: preset.id,
defaults: preset.patch.agents.defaults,
})),
).toStrictEqual([
{
id: "personal",
defaults: {
bootstrapMaxChars: 20_000,
bootstrapTotalMaxChars: 150_000,
contextInjection: "always",
},
},
{
id: "codeAgent",
defaults: {
bootstrapMaxChars: 50_000,
bootstrapTotalMaxChars: 300_000,
contextInjection: "always",
},
},
{
id: "teamBot",
defaults: {
bootstrapMaxChars: 10_000,
bootstrapTotalMaxChars: 80_000,
contextInjection: "continuation-skip",
},
},
{
id: "minimal",
defaults: {
bootstrapMaxChars: 5_000,
bootstrapTotalMaxChars: 30_000,
contextInjection: "continuation-skip",
},
},
]);
for (const preset of CONFIG_PRESETS) {
expect(OpenClawSchema.safeParse(preset.patch).success, preset.id).toBe(true);
}
});
it("returns null when bootstrap defaults are unset", () => {
expect(detectActivePreset({})).toBeNull();
});
it("returns the matching preset when all preset fields match", () => {
expect(
detectActivePreset({
agents: {
defaults: {
bootstrapMaxChars: 50_000,
bootstrapTotalMaxChars: 300_000,
contextInjection: "always",
},
},
}),
).toBe("codeAgent");
});
it("does not match a preset when context injection differs", () => {
expect(
detectActivePreset({
agents: {
defaults: {
bootstrapMaxChars: 50_000,
bootstrapTotalMaxChars: 300_000,
contextInjection: "continuation-skip",
},
},
}),
).toBeNull();
});
});
-121
View File
@@ -1,121 +0,0 @@
/**
* Config presets — opinionated configuration bundles that set multiple
* settings at once. Applied via config.patch.
*/
export type ConfigPresetId = "personal" | "codeAgent" | "teamBot" | "minimal";
type ConfigPresetPatch = {
agents: {
defaults: {
bootstrapMaxChars: number;
bootstrapTotalMaxChars: number;
contextInjection: "always" | "continuation-skip";
};
};
};
type ConfigPreset = {
id: ConfigPresetId;
label: string;
description: string;
icon: string;
patch: ConfigPresetPatch;
};
export const CONFIG_PRESETS: ConfigPreset[] = [
{
id: "personal",
label: "Personal Assistant",
description: "Balanced default for daily use.",
icon: "✨",
patch: {
agents: {
defaults: {
bootstrapMaxChars: 20_000,
bootstrapTotalMaxChars: 150_000,
contextInjection: "always",
},
},
},
},
{
id: "codeAgent",
label: "Code Agent",
description: "Highest context budget for repo work.",
icon: "🛠️",
patch: {
agents: {
defaults: {
bootstrapMaxChars: 50_000,
bootstrapTotalMaxChars: 300_000,
contextInjection: "always",
},
},
},
},
{
id: "teamBot",
label: "Team Bot",
description: "Lean follow-ups for shared bots.",
icon: "👥",
patch: {
agents: {
defaults: {
bootstrapMaxChars: 10_000,
bootstrapTotalMaxChars: 80_000,
contextInjection: "continuation-skip",
},
},
},
},
{
id: "minimal",
label: "Minimal",
description: "Smallest context budget and lowest cost.",
icon: "⚡",
patch: {
agents: {
defaults: {
bootstrapMaxChars: 5_000,
bootstrapTotalMaxChars: 30_000,
contextInjection: "continuation-skip",
},
},
},
},
];
export function getPresetById(id: ConfigPresetId): ConfigPreset | undefined {
return CONFIG_PRESETS.find((p) => p.id === id);
}
/**
* Detect which preset (if any) matches the current config values.
*/
export function detectActivePreset(config: Record<string, unknown>): ConfigPresetId | null {
const agents = config.agents as Record<string, unknown> | undefined;
const defaults = agents?.defaults as Record<string, unknown> | undefined;
if (!defaults) {
return null;
}
const maxChars = defaults.bootstrapMaxChars;
const totalMax = defaults.bootstrapTotalMaxChars;
const contextInjection = defaults.contextInjection;
for (const preset of CONFIG_PRESETS) {
const presetDefaults = (preset.patch.agents as Record<string, unknown>)?.defaults as
| Record<string, unknown>
| undefined;
if (!presetDefaults) {
continue;
}
if (
maxChars === presetDefaults.bootstrapMaxChars &&
totalMax === presetDefaults.bootstrapTotalMaxChars &&
contextInjection === presetDefaults.contextInjection
) {
return preset.id;
}
}
return null;
}
+48 -8
View File
@@ -82,8 +82,6 @@ function createProps(overrides: Partial<QuickSettingsProps> = {}): QuickSettings
setTextScale: vi.fn(),
userAvatar: null,
onUserAvatarChange: vi.fn(),
configObject: {},
onSelectPreset: vi.fn(),
connected: true,
gatewayUrl: "ws://localhost:18789",
assistantName: "OpenClaw",
@@ -139,7 +137,7 @@ describe("renderQuickSettings", () => {
"qs-card--personal",
"qs-card--automations",
]);
expect(container.querySelectorAll(".qs-card--span-all")).toHaveLength(1);
expect(container.querySelectorAll(".qs-card--span-all")).toHaveLength(0);
});
it("renders Gateway host identity and resources", () => {
@@ -276,15 +274,57 @@ describe("renderQuickSettings", () => {
expect(systemCard?.querySelector(".qs-system__address")).toBeNull();
});
it("shows the current bootstrap default when config omits the explicit limit", () => {
it("hides the pending changes bar when the config is clean", () => {
const container = document.createElement("div");
render(renderQuickSettings(createProps({ configObject: {} })), container);
render(renderQuickSettings(createProps()), container);
const summary = container.querySelector(".qs-profiles__summary-values");
expect(summary?.textContent?.replace(/\s+/g, " ").trim()).toBe(
"20,000 chars per file · 60,000 chars total · Every turn",
expect(container.querySelector(".qs-pending")).toBeNull();
});
it("renders pending config actions and calls their handlers", () => {
const onResetConfig = vi.fn();
const onSaveConfig = vi.fn();
const onApplyConfig = vi.fn();
const container = document.createElement("div");
render(
renderQuickSettings(
createProps({
configDirty: true,
configReady: true,
connected: true,
onResetConfig,
onSaveConfig,
onApplyConfig,
}),
),
container,
);
expect(container.querySelector(".qs-pending")).not.toBeNull();
const discardButton = expectButtonByText(container, "Discard");
const saveButton = expectButtonByText(container, "Save");
const applyButton = expectButtonByText(container, "Apply Now");
expect(saveButton.disabled).toBe(false);
discardButton.click();
saveButton.click();
applyButton.click();
expect(onResetConfig).toHaveBeenCalledTimes(1);
expect(onSaveConfig).toHaveBeenCalledTimes(1);
expect(onApplyConfig).toHaveBeenCalledTimes(1);
});
it("disables commit actions until the config is ready", () => {
const container = document.createElement("div");
render(renderQuickSettings(createProps({ configDirty: true, configReady: false })), container);
expect(expectButtonByText(container, "Save").disabled).toBe(true);
expect(expectButtonByText(container, "Apply Now").disabled).toBe(true);
expect(expectButtonByText(container, "Discard").disabled).toBe(false);
});
it("keeps auto as a first-class quick settings fast mode", () => {
+27 -172
View File
@@ -24,12 +24,6 @@ import { formatBytes } from "../../lib/agents/display.ts";
import { resolveAssistantTextAvatar, resolveChatAvatarRenderUrl } from "../../lib/avatar.ts";
import { formatDurationHuman } from "../../lib/format.ts";
import { normalizeOptionalString } from "../../lib/string-coerce.ts";
import {
CONFIG_PRESETS,
detectActivePreset,
getPresetById,
type ConfigPresetId,
} from "./presets.ts";
// ── Types ──
@@ -98,14 +92,11 @@ export type QuickSettingsProps = {
userAvatar?: string | null;
onUserAvatarChange?: (next: string | null) => void;
// Presets
configObject?: Record<string, unknown>;
savedConfigObject?: Record<string, unknown>;
// Pending config changes
configDirty?: boolean;
configSaving?: boolean;
configApplying?: boolean;
configReady?: boolean;
onSelectPreset?: (presetId: ConfigPresetId) => void;
onResetConfig?: () => void;
onSaveConfig?: () => void;
onApplyConfig?: () => void;
@@ -315,51 +306,6 @@ function handleAssistantAvatarFileSelect(e: Event, props: QuickSettingsProps) {
input.value = "";
}
type ProfileSettings = {
bootstrapMaxChars: number;
bootstrapTotalMaxChars: number;
contextInjection: "always" | "continuation-skip";
};
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
bootstrapMaxChars: 20_000,
bootstrapTotalMaxChars: 60_000,
contextInjection: "always",
};
function resolveProfileSettings(config?: Record<string, unknown>): ProfileSettings {
const agents = config?.agents as Record<string, unknown> | undefined;
const defaults = agents?.defaults as Record<string, unknown> | undefined;
const bootstrapMaxChars =
typeof defaults?.bootstrapMaxChars === "number" && Number.isFinite(defaults.bootstrapMaxChars)
? Math.floor(defaults.bootstrapMaxChars)
: DEFAULT_PROFILE_SETTINGS.bootstrapMaxChars;
const bootstrapTotalMaxChars =
typeof defaults?.bootstrapTotalMaxChars === "number" &&
Number.isFinite(defaults.bootstrapTotalMaxChars)
? Math.floor(defaults.bootstrapTotalMaxChars)
: DEFAULT_PROFILE_SETTINGS.bootstrapTotalMaxChars;
const contextInjection =
defaults?.contextInjection === "continuation-skip" ? "continuation-skip" : "always";
return { bootstrapMaxChars, bootstrapTotalMaxChars, contextInjection };
}
function profileSettingsEqual(a: ProfileSettings, b: ProfileSettings): boolean {
return (
a.bootstrapMaxChars === b.bootstrapMaxChars &&
a.bootstrapTotalMaxChars === b.bootstrapTotalMaxChars &&
a.contextInjection === b.contextInjection
);
}
function formatCharBudget(value: number): string {
return `${value.toLocaleString()} chars`;
}
function formatContextInjectionLabel(mode: ProfileSettings["contextInjection"]): string {
return mode === "always" ? "Every turn" : "Skip safe follow-ups";
}
// ── Card renderers ──
function renderCardHeader(icon: TemplateResult, title: string, action?: TemplateResult) {
@@ -984,129 +930,38 @@ function renderPersonalCard(props: QuickSettingsProps) {
`;
}
function renderPresetsCard(props: QuickSettingsProps) {
const draftConfig = props.configObject ?? props.savedConfigObject ?? {};
const savedConfig = props.savedConfigObject ?? {};
const selectedPresetId = detectActivePreset(draftConfig);
const savedPresetId = detectActivePreset(savedConfig);
const selectedPreset = selectedPresetId ? getPresetById(selectedPresetId) : undefined;
const savedPreset = savedPresetId ? getPresetById(savedPresetId) : undefined;
const draftSettings = resolveProfileSettings(draftConfig);
const savedSettings = resolveProfileSettings(savedConfig);
const hasPendingProfileChange = !profileSettingsEqual(draftSettings, savedSettings);
const hasPendingConfigChange = props.configDirty === true;
function renderPendingChangesBar(props: QuickSettingsProps) {
if (props.configDirty !== true) {
return nothing;
}
const canCommit =
props.connected &&
props.configReady === true &&
props.configSaving !== true &&
props.configApplying !== true;
const commitHint = hasPendingProfileChange
? "Save writes this profile as the default. Apply Now also reloads the current session."
: "Staged config edits are pending. Saving commits all staged changes.";
return html`
<div class="qs-card qs-card--span-all">
${renderCardHeader(
icons.zap,
"Context Profile",
hasPendingProfileChange
? html`<span class="qs-badge qs-badge--warn">Pending</span>`
: savedPreset
? html`<span class="qs-badge qs-badge--ok">Saved</span>`
: html`<span class="qs-badge">Custom</span>`,
)}
<div class="qs-card__body qs-profiles">
<p class="qs-profiles__intro">
Choose how much workspace context OpenClaw injects into each run. Profiles only change
bootstrap size and follow-up reinjection — never your model, tools, channels, or theme.
</p>
<div class="qs-presets-grid">
${CONFIG_PRESETS.map((preset) => {
const presetDefaults = ((preset.patch.agents as Record<string, unknown> | undefined)
?.defaults ?? {}) as Record<string, unknown>;
const presetContext =
presetDefaults.contextInjection === "continuation-skip"
? "continuation-skip"
: "always";
return html`
<button
type="button"
class="qs-preset ${preset.id === selectedPresetId ? "qs-preset--active" : ""}"
aria-pressed=${preset.id === selectedPresetId}
@click=${() => props.onSelectPreset?.(preset.id)}
>
<div class="qs-preset__head">
<div class="qs-preset__identity">
<span class="qs-preset__icon">${preset.icon}</span>
<div class="qs-preset__identity-copy">
<span class="qs-preset__label">${preset.label}</span>
<span class="qs-preset__desc muted">${preset.description}</span>
</div>
</div>
<div class="qs-preset__badges">
${preset.id === savedPresetId
? html`<span class="qs-badge qs-badge--ok">Current</span>`
: nothing}
${hasPendingProfileChange && preset.id === selectedPresetId
? html`<span class="qs-badge qs-badge--warn">Selected</span>`
: nothing}
</div>
</div>
<div class="qs-preset__meta">
<span
>${formatCharBudget(Number(presetDefaults.bootstrapMaxChars ?? 0))} per
file</span
>
<span
>${formatCharBudget(Number(presetDefaults.bootstrapTotalMaxChars ?? 0))}
total</span
>
<span>${formatContextInjectionLabel(presetContext)}</span>
</div>
</button>
`;
})}
</div>
<div class="qs-profiles__footer" aria-live="polite">
<div class="qs-profiles__summary">
<span class="qs-profiles__summary-label"
>${selectedPreset?.label ?? "Custom values"}</span
>
<span class="qs-profiles__summary-values"
>${formatCharBudget(draftSettings.bootstrapMaxChars)} per file ·
${formatCharBudget(draftSettings.bootstrapTotalMaxChars)} total ·
${formatContextInjectionLabel(draftSettings.contextInjection)}</span
>
</div>
${hasPendingConfigChange
? html`
<div class="qs-profiles__actions">
<span class="qs-profiles__hint muted">${commitHint}</span>
<button
class="btn btn--sm"
?disabled=${props.configSaving === true || props.configApplying === true}
@click=${props.onResetConfig}
>
Discard
</button>
<button
class="btn btn--sm primary"
?disabled=${!canCommit}
@click=${props.onSaveConfig}
>
${props.configSaving === true
? "Saving…"
: hasPendingProfileChange
? "Save Profile"
: "Save Changes"}
</button>
<button class="btn btn--sm" ?disabled=${!canCommit} @click=${props.onApplyConfig}>
${props.configApplying === true ? "Applying…" : "Apply Now"}
</button>
</div>
`
: nothing}
</div>
<div class="qs-card qs-card--span-all qs-pending" aria-live="polite">
<div class="qs-pending__copy">
<span class="qs-pending__label">Unsaved changes</span>
<span class="qs-pending__hint muted"
>Save stores your changes. Apply Now also reloads the current session.</span
>
</div>
<div class="qs-pending__actions">
<button
class="btn btn--sm"
?disabled=${props.configSaving === true || props.configApplying === true}
@click=${props.onResetConfig}
>
Discard
</button>
<button class="btn btn--sm primary" ?disabled=${!canCommit} @click=${props.onSaveConfig}>
${props.configSaving === true ? "Saving…" : "Save"}
</button>
<button class="btn btn--sm" ?disabled=${!canCommit} @click=${props.onApplyConfig}>
${props.configApplying === true ? "Applying…" : "Apply Now"}
</button>
</div>
</div>
`;
@@ -1133,7 +988,7 @@ export function renderQuickSettings(props: QuickSettingsProps) {
<div class="qs-grid">
${renderModelCard(props)} ${renderChannelsCard(props)} ${renderSecurityCard(props)}
${renderSystemCard(props)} ${renderAppearanceCard(props)} ${renderPersonalCard(props)}
${renderAutomationsCard(props)} ${renderPresetsCard(props)}
${renderAutomationsCard(props)} ${renderPendingChangesBar(props)}
</div>
${renderConnectionFooter(props)}
+16 -150
View File
@@ -955,173 +955,39 @@ openclaw-logs-page {
height: 14px;
}
/* ── Presets Grid ── */
/* ── Pending Changes ── */
.qs-profiles {
display: grid;
gap: 14px;
padding: 14px 16px 16px;
}
.qs-profiles__intro {
margin: 0;
max-width: 78ch;
font-size: 0.8125rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.qs-presets-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
gap: 10px;
}
.qs-profiles__footer {
.qs-pending {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px 16px;
padding-top: 12px;
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
gap: 12px 16px;
padding: 14px 16px;
}
.qs-profiles__summary {
.qs-pending__copy {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 10px;
min-width: 0;
flex-direction: column;
gap: 3px;
}
.qs-profiles__summary-label {
font-size: 0.8125rem;
font-weight: 650;
.qs-pending__label {
font-weight: 600;
color: var(--text-strong);
}
.qs-profiles__summary-values {
font-size: 0.75rem;
color: var(--muted);
}
.qs-profiles__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.qs-profiles__hint {
.qs-pending__hint {
font-size: 0.75rem;
line-height: 1.4;
max-width: 46ch;
}
.qs-preset {
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px;
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: var(--radius-lg);
background: var(--card);
cursor: pointer;
transition:
border-color var(--duration-normal) var(--ease-out),
background var(--duration-normal) var(--ease-out),
box-shadow var(--duration-normal) var(--ease-out),
transform var(--duration-normal) var(--ease-out);
text-align: left;
touch-action: manipulation;
}
.qs-preset:hover {
border-color: color-mix(in srgb, var(--accent) 40%, var(--border) 60%);
background: color-mix(in srgb, var(--accent-subtle) 40%, var(--card) 60%);
}
.qs-preset:focus-visible {
outline: none;
border-color: color-mix(in srgb, var(--accent) 55%, var(--border) 45%);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent);
}
.qs-preset--active {
border-color: color-mix(in srgb, var(--accent) 50%, var(--border) 50%);
background: color-mix(in srgb, var(--accent-subtle) 60%, var(--card) 40%);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 15%, transparent);
}
.qs-preset--active:hover {
background: color-mix(in srgb, var(--accent-subtle) 80%, var(--card) 20%);
}
.qs-preset__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.qs-preset__identity {
min-width: 0;
display: flex;
align-items: flex-start;
gap: 10px;
}
.qs-preset__identity-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.qs-preset__badges {
.qs-pending__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.qs-preset__icon {
font-size: 1.45rem;
line-height: 1;
flex: 0 0 auto;
}
.qs-preset__label {
font-size: 0.875rem;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-strong);
}
.qs-preset__desc {
font-size: 0.75rem;
line-height: 1.45;
color: var(--muted);
text-wrap: pretty;
}
.qs-preset__meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 0.6875rem;
color: var(--muted);
}
.qs-preset__meta span {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--bg-elevated) 84%, transparent);
justify-content: flex-end;
gap: 8px;
}
@media (max-width: 1100px) {
@@ -1155,13 +1021,13 @@ openclaw-logs-page {
grid-template-columns: 1fr;
}
.qs-profiles__footer,
.qs-profiles__actions {
.qs-pending,
.qs-pending__actions {
flex-direction: column;
align-items: stretch;
}
.qs-profiles__actions .btn {
.qs-pending__actions .btn {
width: 100%;
justify-content: center;
}