diff --git a/scripts/control-ui-mock-dev.ts b/scripts/control-ui-mock-dev.ts index 2e4861ddc264..ba7251ba55d3 100644 --- a/scripts/control-ui-mock-dev.ts +++ b/scripts/control-ui-mock-dev.ts @@ -14,6 +14,7 @@ import type { } from "../packages/gateway-protocol/src/index.js"; import { expectDefined } from "../packages/normalization-core/src/expect.js"; import { applySharedChannelFieldHelp } from "../src/config/schema.channel-field-help.js"; +import { buildBaseHints } from "../src/config/schema.hints.js"; import { applyConfigTierHints, applyResolvedConfigTierHints } from "../src/config/schema.tiers.js"; import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../src/gateway/control-ui-contract.js"; import { @@ -851,7 +852,9 @@ function buildConfigMocks(options: { swarmEnabled?: boolean } = {}) { uiHints: applySharedChannelFieldHelp( applyResolvedConfigTierHints( schema, - applyConfigTierHints({}, { includePluginOwnedChannels: true }), + // Seed with base hints so the mock carries the gateway's labels, + // help, and docsUrl metadata instead of bare tier scaffolding. + applyConfigTierHints(buildBaseHints(), { includePluginOwnedChannels: true }), ), ), version: "mock-config-schema", diff --git a/src/config/schema.hints.test.ts b/src/config/schema.hints.test.ts index a5a3406eec15..ca28db2d9f00 100644 --- a/src/config/schema.hints.test.ts +++ b/src/config/schema.hints.test.ts @@ -8,9 +8,11 @@ import { buildSecretInputSchema } from "../plugin-sdk/secret-input-schema.js"; import { buildBaseHints, testApi } from "./schema.hints.js"; import { isSensitiveConfigPath } from "./sensitive-paths.js"; import { OpenClawSchema } from "./zod-schema.js"; +import { OpenClawSchemaShape } from "./zod-schema.root-shape.js"; import { sensitive } from "./zod-schema.sensitive.js"; -const { collectMatchingSchemaPaths, mapSensitivePaths, SECTION_DOCS_URLS } = testApi; +const { collectMatchingSchemaPaths, mapSensitivePaths, SECTION_DOCS_URLS, SECTIONS_WITHOUT_DOCS } = + testApi; const BUNDLED_CHANNEL_HINT_PREFIXES = [ "channels.discord", "channels.imessage", @@ -23,6 +25,18 @@ const BUNDLED_CHANNEL_HINT_PREFIXES = [ ] as const; describe("section docs URLs", () => { + it("accounts for every root config section", () => { + const sectionsWithDocsDecisions = new Set([ + ...Object.keys(SECTION_DOCS_URLS), + ...SECTIONS_WITHOUT_DOCS, + ]); + const undecidedSections = Object.keys(OpenClawSchemaShape).filter( + (section) => !sectionsWithDocsDecisions.has(section), + ); + + expect(undecidedSections).toEqual([]); + }); + it("maps every URL to an existing task-oriented docs page", () => { const hints = buildBaseHints(); const docsOrigin = "https://docs.openclaw.ai"; diff --git a/src/config/schema.hints.ts b/src/config/schema.hints.ts index 1669990105ec..6530f8c4c15a 100644 --- a/src/config/schema.hints.ts +++ b/src/config/schema.hints.ts @@ -47,6 +47,7 @@ const GROUP_HINTS = [ // docsUrl targets task-oriented or beginner pages; configuration-reference anchors are banned. const SECTION_DOCS_URLS = { + accessGroups: "https://docs.openclaw.ai/channels/access-groups", messages: "https://docs.openclaw.ai/concepts/messages", tts: "https://docs.openclaw.ai/tts", commands: "https://docs.openclaw.ai/tools/slash-commands", @@ -85,8 +86,15 @@ const SECTION_DOCS_URLS = { presence: "https://docs.openclaw.ai/concepts/presence", cloudWorkers: "https://docs.openclaw.ai/gateway/cloud-workers", worktrees: "https://docs.openclaw.ai/concepts/managed-worktrees", + proxy: "https://docs.openclaw.ai/security/network-proxy", + transcripts: "https://docs.openclaw.ai/plugins/meeting-plugins", + surfaces: "https://docs.openclaw.ai/concepts/messages", } as const satisfies Record; +// Root sections without beginner-worthy pages stay explicit. Adding a root config key +// requires choosing a docsUrl or listing it here. +const SECTIONS_WITHOUT_DOCS = ["$schema", "meta", "attachments"] as const; + const FIELD_PLACEHOLDERS: Record = { "gateway.remote.url": "ws://host:18789", "gateway.remote.tlsFingerprint": "sha256:ab12cd34…", @@ -325,4 +333,5 @@ export const testApi = { collectMatchingSchemaPaths, mapSensitivePaths, SECTION_DOCS_URLS, + SECTIONS_WITHOUT_DOCS, }; diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index b7c8e370ced4..ae8522215bac 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -1142,6 +1142,7 @@ export const en: TranslationMap = { // keys now serve Models, General, Privacy & Security, Appearance, and Profile. // Renaming would force retranslation of every key, so the name stays. quickSettings: { + intro: "Settings sync to your Gateway configuration file.", language: "Language", model: { title: "Model & Thinking", @@ -1348,6 +1349,7 @@ export const en: TranslationMap = { "Notifications are disabled for OpenClaw in macOS. Allow them in System Settings > Notifications.", }, appearance: { + intro: "Theme, chat, and sidebar preferences for this Control UI client.", theme: "Theme", chooseTheme: "Choose a theme family.", importedTheme: "Imported theme", diff --git a/ui/src/pages/config/memory-page.test.ts b/ui/src/pages/config/memory-page.test.ts index a57bb7d2fc20..85450b065f36 100644 --- a/ui/src/pages/config/memory-page.test.ts +++ b/ui/src/pages/config/memory-page.test.ts @@ -619,6 +619,31 @@ describe("MemorySettingsPage tab routing", () => { }); describe("MemorySettingsPage dreaming support", () => { + it("links the dreaming intro to its guide", async () => { + const { element } = createPage({ + configObject: {}, + routeData: memoryTabRoute("settings"), + }); + document.body.append(element); + try { + await waitForFast(() => + expect( + element.querySelector( + '.settings-page__intro a[href="https://docs.openclaw.ai/concepts/dreaming"]', + ), + ).not.toBeNull(), + ); + const link = element.querySelector( + '.settings-page__intro a[href="https://docs.openclaw.ai/concepts/dreaming"]', + ); + + expect(link?.textContent?.trim()).toBe("Learn more"); + expect(link?.href).toBe("https://docs.openclaw.ai/concepts/dreaming"); + } finally { + element.remove(); + } + }); + it("re-probes after reconnect and drops the abandoned capability result", async () => { const first = deferred(); const second = deferred(); diff --git a/ui/src/pages/config/memory-page.ts b/ui/src/pages/config/memory-page.ts index 51d9066ed6da..5efa2732476e 100644 --- a/ui/src/pages/config/memory-page.ts +++ b/ui/src/pages/config/memory-page.ts @@ -9,6 +9,7 @@ import type { DoctorMemoryStatusPayload } from "../../../../src/gateway/server-m import { pathForMemoryTab } from "../../app-route-paths.ts"; import { applicationContext, type ApplicationContext } from "../../app/context.ts"; import type { AgentSelectOption } from "../../components/agent-select.ts"; +import { renderDocsLink } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; import { listSelectableAgents, normalizeAgentLabel } from "../../lib/agents/display.ts"; import { currentConfigObject } from "../../lib/config/index.ts"; @@ -55,6 +56,7 @@ const MEMORY_ADDON_PLUGINS = [ /** Explicit-off sentinel; resolveSlotSelection maps it to an `off` selection. */ const MEMORY_SLOT_OFF = "none"; const MEMORY_SLOT_PATH = ["plugins", "slots", "memory"]; +const DREAMING_DOCS_URL = "https://docs.openclaw.ai/concepts/dreaming"; type GatewayClient = NonNullable; @@ -455,7 +457,10 @@ class MemorySettingsPage extends OpenClawLightDomElement { private renderDreamingControls() { const pluginId = this.dreamingPluginId(); return html` -

${t("memoryPage.dreaming.intro", { plugin: pluginId })}

+

+ ${t("memoryPage.dreaming.intro", { plugin: pluginId })} + ${renderDocsLink(DREAMING_DOCS_URL, t("common.learnMore"))} +

${this.support === "unsupported" ? renderDreamingUnsupported(pluginId) : renderDreamingSettings({ diff --git a/ui/src/pages/config/quick.test.ts b/ui/src/pages/config/quick.test.ts index 15ea60eabf37..13cfe79e5cf7 100644 --- a/ui/src/pages/config/quick.test.ts +++ b/ui/src/pages/config/quick.test.ts @@ -51,6 +51,11 @@ describe("renderQuickSettings", () => { expect(container.querySelector(".config-host")).toBeNull(); expect(container.querySelectorAll(".settings-group")).toHaveLength(1); expect(container.querySelector(".settings-group .settings-group")).toBeNull(); + const intro = container.querySelector(".settings-page__intro"); + expect(intro?.textContent).toContain("Settings sync to your Gateway configuration file."); + expect(intro?.querySelector("a")?.href).toBe( + "https://docs.openclaw.ai/gateway/configuration", + ); }); it("changes the Control UI language from General settings", () => { diff --git a/ui/src/pages/config/quick.ts b/ui/src/pages/config/quick.ts index be940fb39b48..1f3ba29caf57 100644 --- a/ui/src/pages/config/quick.ts +++ b/ui/src/pages/config/quick.ts @@ -8,6 +8,7 @@ import { html, nothing } from "lit"; import { + renderDocsLink, renderSettingsNavRow, renderSettingsPage, renderSettingsRow, @@ -18,6 +19,8 @@ import type { ConfigAutoSaveStatus } from "../../lib/config/index.ts"; import { renderLanguageSelect } from "./language-select.ts"; import { renderConfigApplyBanner, renderConfigAutoSaveStatus } from "./view.ts"; +const GENERAL_DOCS_URL = "https://docs.openclaw.ai/gateway/configuration"; + // ── Types ── type QuickSettingsProps = { @@ -94,6 +97,9 @@ export function renderQuickSettings(props: QuickSettingsProps) { connected: props.connected, onApply: () => props.onApplyConfig?.(), })} +

+ ${t("quickSettings.intro")} ${renderDocsLink(GENERAL_DOCS_URL, t("common.learnMore"))} +

${renderGeneralSection(props)} `); } diff --git a/ui/src/pages/config/view-appearance.ts b/ui/src/pages/config/view-appearance.ts index 8f4127480d32..cec62bdf79b2 100644 --- a/ui/src/pages/config/view-appearance.ts +++ b/ui/src/pages/config/view-appearance.ts @@ -4,6 +4,7 @@ import type { ThemeTransitionContext } from "../../app/theme-transition.ts"; import type { ThemeName } from "../../app/theme.ts"; import { icons } from "../../components/icons.ts"; import { + renderDocsLink, renderSettingsRow, renderSettingsStatus, renderSettingsValue, @@ -17,6 +18,8 @@ import { } from "./view-appearance-preferences.ts"; import type { ConfigProps } from "./view-types.ts"; +const APPEARANCE_DOCS_URL = "https://docs.openclaw.ai/web/control-ui"; + const TEXT_SCALE_LABELS: Record = { 90: "configView.textSizes.small", 100: "configView.textSizes.default", @@ -125,6 +128,10 @@ export function renderAppearanceSection( ]; return html`
+

+ ${t("configView.appearance.intro")} + ${renderDocsLink(APPEARANCE_DOCS_URL, t("common.learnMore"))} +

${t("configView.appearance.theme")}

diff --git a/ui/src/pages/model-providers/model-providers-page.test.ts b/ui/src/pages/model-providers/model-providers-page.test.ts index 539be76c5e46..d54fbc557be7 100644 --- a/ui/src/pages/model-providers/model-providers-page.test.ts +++ b/ui/src/pages/model-providers/model-providers-page.test.ts @@ -156,6 +156,16 @@ afterEach(() => { }); describe("ModelProvidersPage agent scope", () => { + it("links the page subtitle to the model providers guide", async () => { + const { context } = createHarness("main"); + const page = appendPage(context); + await page.updateComplete; + + const link = page.querySelector(".page-subtitle a"); + expect(link?.textContent?.trim()).toBe("Learn more"); + expect(link?.href).toBe("https://docs.openclaw.ai/concepts/model-providers"); + }); + it("patches thinking and fast mode through the shared config draft", async () => { const { context, runtimeConfig } = createHarness("main"); const page = appendPage(context); diff --git a/ui/src/pages/model-providers/model-providers-page.ts b/ui/src/pages/model-providers/model-providers-page.ts index dbaa2191cf9e..42b36eb20818 100644 --- a/ui/src/pages/model-providers/model-providers-page.ts +++ b/ui/src/pages/model-providers/model-providers-page.ts @@ -9,6 +9,7 @@ import { titleForRoute } from "../../app-navigation.ts"; import { applicationContext, type ApplicationContext } from "../../app/context.ts"; import { hasOperatorAdminAccess } from "../../app/operator-access.ts"; import { renderAgentScopeControl } from "../../components/agent-scope-control.ts"; +import { renderDocsLink } from "../../components/settings-ui.ts"; import { renderSettingsWorkspace } from "../../components/settings-workspace.ts"; import { t } from "../../i18n/index.ts"; import { normalizeAgentLabel } from "../../lib/agents/display.ts"; @@ -37,6 +38,8 @@ import { } from "./mutations.ts"; import { renderModelProviders, type ModelProviderRowMessage } from "./view.ts"; +const MODEL_PROVIDERS_DOCS_URL = "https://docs.openclaw.ai/concepts/model-providers"; + export type ModelProvidersRouteData = { data: ModelProvidersData; /** Client the loader fetched from; null when it ran disconnected. */ @@ -570,12 +573,8 @@ export class ModelProvidersPage extends OpenClawLightDomElement { override render() { const gatewaySnapshot = this.context.gateway.snapshot; const agents = this.context.agents.state.agentsList?.agents ?? []; - const selectedAgent = agents.find( - (agent) => normalizeAgentId(agent.id) === this.selectedAgentId, - ); - const selectedAgentLabel = selectedAgent - ? normalizeAgentLabel(selectedAgent) - : this.selectedAgentId; + const selected = agents.find((agent) => normalizeAgentId(agent.id) === this.selectedAgentId); + const selectedAgentLabel = selected ? normalizeAgentLabel(selected) : this.selectedAgentId; const data = this.data ?? EMPTY_MODEL_PROVIDERS_DATA; const config = readModelProviderConfig(data.config); const defaults = this.defaultsDraft ?? config.defaults; @@ -588,11 +587,8 @@ export class ModelProvidersPage extends OpenClawLightDomElement { const agentsDefaults = asConfigRecord(asConfigRecord(configObject.agents)?.defaults); const thinkingLevel = typeof agentsDefaults?.thinkingDefault === "string" ? agentsDefaults.thinkingDefault : "off"; - const configuredFastMode = agentsDefaults?.fastModeDefault; - const fastMode = - configuredFastMode === "auto" || typeof configuredFastMode === "boolean" - ? configuredFastMode - : false; + const fastValue = agentsDefaults?.fastModeDefault; + const fastMode = fastValue === "auto" || typeof fastValue === "boolean" ? fastValue : false; const update = this.context.overlays.snapshot; // The overlay update states replace General's old configUpdating prop, // which config-page derived from this same snapshot (isUpdateBusy); the @@ -717,6 +713,10 @@ export class ModelProvidersPage extends OpenClawLightDomElement {
${titleForRoute("model-providers")}
+
+ ${t("modelProviders.subtitle")} + ${renderDocsLink(MODEL_PROVIDERS_DOCS_URL, t("common.learnMore"))} +
${renderAgentScopeControl({ diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 6be791d3a84e..7913b76e422d 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -3716,6 +3716,14 @@ html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native- color: var(--accent); } +.page-subtitle { + max-width: 52ch; + margin-top: 2px; + font-size: var(--control-ui-text-sm); + line-height: 1.45; + color: var(--muted); +} + .page-meta { display: flex; gap: 8px;