diff --git a/ui/src/i18n/.i18n/raw-copy-baseline.json b/ui/src/i18n/.i18n/raw-copy-baseline.json index 3eae98a935a0..e7e3860afe4f 100644 --- a/ui/src/i18n/.i18n/raw-copy-baseline.json +++ b/ui/src/i18n/.i18n/raw-copy-baseline.json @@ -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", diff --git a/ui/src/lib/config/index.test.ts b/ui/src/lib/config/index.test.ts index 490e5c6cade3..27b5b936a682 100644 --- a/ui/src/lib/config/index.test.ts +++ b/ui/src/lib/config/index.test.ts @@ -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(); diff --git a/ui/src/lib/config/index.ts b/ui/src/lib/config/index.ts index 676577bcf499..e458687511bd 100644 --- a/ui/src/lib/config/index.ts +++ b/ui/src/lib/config/index.ts @@ -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) => void; setRaw: (value: string) => void; resetDraft: () => void; - stagePreset: (patch: Record) => void; save: () => Promise; apply: () => Promise; openFile: () => Promise; @@ -634,20 +632,6 @@ export function updateConfigRawValue(state: ConfigState, value: string) { } } -export function stageConfigPreset(state: ConfigState, patch: Record) { - 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)); -} - 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)), diff --git a/ui/src/pages/config/config-page.ts b/ui/src/pages/config/config-page.ts index 3cd614786136..65cce9f5cb2b 100644 --- a/ui/src/pages/config/config-page.ts +++ b/ui/src/pages/config/config-page.ts @@ -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(), diff --git a/ui/src/pages/config/presets.test.ts b/ui/src/pages/config/presets.test.ts deleted file mode 100644 index dacbc5ee4a5a..000000000000 --- a/ui/src/pages/config/presets.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/ui/src/pages/config/presets.ts b/ui/src/pages/config/presets.ts deleted file mode 100644 index 2671cfff0cd3..000000000000 --- a/ui/src/pages/config/presets.ts +++ /dev/null @@ -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): ConfigPresetId | null { - const agents = config.agents as Record | undefined; - const defaults = agents?.defaults as Record | 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)?.defaults as - | Record - | undefined; - if (!presetDefaults) { - continue; - } - if ( - maxChars === presetDefaults.bootstrapMaxChars && - totalMax === presetDefaults.bootstrapTotalMaxChars && - contextInjection === presetDefaults.contextInjection - ) { - return preset.id; - } - } - return null; -} diff --git a/ui/src/pages/config/quick.test.ts b/ui/src/pages/config/quick.test.ts index 32265fb291bd..c9e1f689e1e6 100644 --- a/ui/src/pages/config/quick.test.ts +++ b/ui/src/pages/config/quick.test.ts @@ -82,8 +82,6 @@ function createProps(overrides: Partial = {}): 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", () => { diff --git a/ui/src/pages/config/quick.ts b/ui/src/pages/config/quick.ts index 358a97cef9d6..174f42e8069b 100644 --- a/ui/src/pages/config/quick.ts +++ b/ui/src/pages/config/quick.ts @@ -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; - savedConfigObject?: Record; + // 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): ProfileSettings { - const agents = config?.agents as Record | undefined; - const defaults = agents?.defaults as Record | 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` -
- ${renderCardHeader( - icons.zap, - "Context Profile", - hasPendingProfileChange - ? html`Pending` - : savedPreset - ? html`Saved` - : html`Custom`, - )} -
-

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

-
- ${CONFIG_PRESETS.map((preset) => { - const presetDefaults = ((preset.patch.agents as Record | undefined) - ?.defaults ?? {}) as Record; - const presetContext = - presetDefaults.contextInjection === "continuation-skip" - ? "continuation-skip" - : "always"; - return html` - - `; - })} -
- +
+
+ Unsaved changes + Save stores your changes. Apply Now also reloads the current session. +
+
+ + +
`; @@ -1133,7 +988,7 @@ export function renderQuickSettings(props: QuickSettingsProps) {
${renderModelCard(props)} ${renderChannelsCard(props)} ${renderSecurityCard(props)} ${renderSystemCard(props)} ${renderAppearanceCard(props)} ${renderPersonalCard(props)} - ${renderAutomationsCard(props)} ${renderPresetsCard(props)} + ${renderAutomationsCard(props)} ${renderPendingChangesBar(props)}
${renderConnectionFooter(props)} diff --git a/ui/src/styles/config-quick.css b/ui/src/styles/config-quick.css index 6cc62e435d77..a7da8a461ecb 100644 --- a/ui/src/styles/config-quick.css +++ b/ui/src/styles/config-quick.css @@ -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; }