mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
ui: complete contextual Settings documentation links (#115665)
* feat(config): require docs decisions for root settings * feat(ui): link settings pages to guides * refactor(ui): keep model providers page compact * fix(ui): style page subtitles and carry base hints in mock dev
This commit is contained in:
committed by
GitHub
parent
6090386dc7
commit
6fbaa4d232
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string, string>;
|
||||
|
||||
// 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<string, string> = {
|
||||
"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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<HTMLAnchorElement>(
|
||||
'.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<unknown>();
|
||||
const second = deferred<unknown>();
|
||||
|
||||
@@ -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<ApplicationContext["gateway"]["snapshot"]["client"]>;
|
||||
|
||||
@@ -455,7 +457,10 @@ class MemorySettingsPage extends OpenClawLightDomElement {
|
||||
private renderDreamingControls() {
|
||||
const pluginId = this.dreamingPluginId();
|
||||
return html`
|
||||
<p class="settings-page__intro">${t("memoryPage.dreaming.intro", { plugin: pluginId })}</p>
|
||||
<p class="settings-page__intro">
|
||||
${t("memoryPage.dreaming.intro", { plugin: pluginId })}
|
||||
${renderDocsLink(DREAMING_DOCS_URL, t("common.learnMore"))}
|
||||
</p>
|
||||
${this.support === "unsupported"
|
||||
? renderDreamingUnsupported(pluginId)
|
||||
: renderDreamingSettings({
|
||||
|
||||
@@ -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<HTMLAnchorElement>("a")?.href).toBe(
|
||||
"https://docs.openclaw.ai/gateway/configuration",
|
||||
);
|
||||
});
|
||||
|
||||
it("changes the Control UI language from General settings", () => {
|
||||
|
||||
@@ -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?.(),
|
||||
})}
|
||||
<p class="settings-page__intro">
|
||||
${t("quickSettings.intro")} ${renderDocsLink(GENERAL_DOCS_URL, t("common.learnMore"))}
|
||||
</p>
|
||||
${renderGeneralSection(props)}
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -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<TextScaleStop, string> = {
|
||||
90: "configView.textSizes.small",
|
||||
100: "configView.textSizes.default",
|
||||
@@ -125,6 +128,10 @@ export function renderAppearanceSection(
|
||||
];
|
||||
return html`
|
||||
<div class="settings-page">
|
||||
<p class="settings-page__intro">
|
||||
${t("configView.appearance.intro")}
|
||||
${renderDocsLink(APPEARANCE_DOCS_URL, t("common.learnMore"))}
|
||||
</p>
|
||||
<section id=${APPEARANCE_SETTINGS_TARGET_IDS.theme} class="settings-section">
|
||||
<div class="settings-section__header">
|
||||
<h2 class="settings-section__heading">${t("configView.appearance.theme")}</h2>
|
||||
|
||||
@@ -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<HTMLAnchorElement>(".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);
|
||||
|
||||
@@ -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 {
|
||||
<section class="content-header">
|
||||
<div>
|
||||
<div class="page-title">${titleForRoute("model-providers")}</div>
|
||||
<div class="page-subtitle">
|
||||
${t("modelProviders.subtitle")}
|
||||
${renderDocsLink(MODEL_PROVIDERS_DOCS_URL, t("common.learnMore"))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
${renderAgentScopeControl({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user