mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
Add browser and tool profile quick settings (#80609)
Summary: - The PR adds Browser enabled and Tool profile controls to Control UI Quick Settings, stacks the tool profile row for narrow cards, and adds focused UI tests plus a screenshot asset. - Reproducibility: not applicable. This PR adds a Control UI quick-settings capability rather than fixing a reported reproducible bug. Source inspection and the supplied screenshot/log proof cover the changed behavior. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for head06adff19fd. - Required merge gates passed before the squash merge. Prepared head SHA:06adff19fdReview: https://github.com/openclaw/openclaw/pull/80609#issuecomment-4419255136 Co-authored-by: Jeff J Hunter <support@aipersonamethod.com>
This commit is contained in:
BIN
docs/assets/pr/quick-settings-browser-tools.png
Normal file
BIN
docs/assets/pr/quick-settings-browser-tools.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -214,6 +214,23 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.qs-row--tool-profile {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-block: 10px;
|
||||
}
|
||||
|
||||
.qs-row--tool-profile .qs-row__label {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qs-row--tool-profile .qs-segmented {
|
||||
align-self: stretch;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.qs-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
@@ -241,17 +241,29 @@ describe("renderApp assistant avatar routing", () => {
|
||||
expect(shell?.style.getPropertyValue("--chat-message-max-width")).toBe("min(1280px, 82%)");
|
||||
});
|
||||
|
||||
it("passes tools.exec.security to Quick Settings", () => {
|
||||
renderApp(
|
||||
createState({
|
||||
configForm: {
|
||||
tools: { exec: { security: "full" } },
|
||||
agents: { defaults: { exec: { security: "deny" } } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
it("passes security quick setting fields to Quick Settings", () => {
|
||||
const state = createState({
|
||||
configForm: {
|
||||
browser: { enabled: false },
|
||||
tools: { profile: "messaging", exec: { security: "full" } },
|
||||
agents: { defaults: { exec: { security: "deny" } } },
|
||||
},
|
||||
});
|
||||
|
||||
renderApp(state);
|
||||
|
||||
expect(quickSettingsProps.current?.security.execPolicy).toBe("full");
|
||||
expect(quickSettingsProps.current?.security.browserEnabled).toBe(false);
|
||||
expect(quickSettingsProps.current?.security.toolProfile).toBe("messaging");
|
||||
|
||||
quickSettingsProps.current?.onBrowserEnabledToggle?.(true);
|
||||
quickSettingsProps.current?.onToolProfileChange?.("full");
|
||||
|
||||
expect(state.configForm?.browser).toEqual({ enabled: true });
|
||||
expect(state.configForm?.tools).toMatchObject({
|
||||
profile: "full",
|
||||
exec: { security: "full" },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders stale cron state containing a job without a payload", () => {
|
||||
|
||||
@@ -31,6 +31,22 @@ describe("extractQuickSettingsSecurity", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reads browser enabled and tool profile from canonical config paths", () => {
|
||||
const result = extractQuickSettingsSecurity(
|
||||
makeState({ browser: { enabled: false }, tools: { profile: "messaging" } }),
|
||||
);
|
||||
|
||||
expect(result.browserEnabled).toBe(false);
|
||||
expect(result.toolProfile).toBe("messaging");
|
||||
});
|
||||
|
||||
it("uses effective quick settings defaults when browser and tool profile are unset", () => {
|
||||
const result = extractQuickSettingsSecurity(makeState({}));
|
||||
|
||||
expect(result.browserEnabled).toBe(true);
|
||||
expect(result.toolProfile).toBe("full");
|
||||
});
|
||||
|
||||
it("ignores agents.defaults.exec.security because it is not a schema path", () => {
|
||||
const result = extractQuickSettingsSecurity(
|
||||
makeState({
|
||||
@@ -58,5 +74,8 @@ describe("extractQuickSettingsSecurity", () => {
|
||||
expect(
|
||||
extractQuickSettingsSecurity(makeState({ tools: { exec: { security: " " } } })).execPolicy,
|
||||
).toBe("allowlist");
|
||||
expect(
|
||||
extractQuickSettingsSecurity(makeState({ tools: { profile: " coding " } })).toolProfile,
|
||||
).toBe("coding");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -549,10 +549,18 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
|
||||
gatewayAuth: string;
|
||||
execPolicy: string;
|
||||
deviceAuth: boolean;
|
||||
browserEnabled: boolean;
|
||||
toolProfile: string;
|
||||
} {
|
||||
const config = state.configForm ?? state.configSnapshot?.config;
|
||||
if (!config || typeof config !== "object") {
|
||||
return { gatewayAuth: "unknown", execPolicy: "unknown", deviceAuth: false };
|
||||
return {
|
||||
gatewayAuth: "unknown",
|
||||
execPolicy: "unknown",
|
||||
deviceAuth: false,
|
||||
browserEnabled: true,
|
||||
toolProfile: "full",
|
||||
};
|
||||
}
|
||||
const cfg = config;
|
||||
const gateway =
|
||||
@@ -579,8 +587,16 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
|
||||
}
|
||||
}
|
||||
let execPolicy = "allowlist";
|
||||
let toolProfile = "full";
|
||||
const tools = cfg.tools;
|
||||
if (tools && typeof tools === "object") {
|
||||
const profile = (tools as Record<string, unknown>).profile;
|
||||
if (typeof profile === "string") {
|
||||
const trimmedProfile = profile.trim();
|
||||
if (trimmedProfile) {
|
||||
toolProfile = trimmedProfile;
|
||||
}
|
||||
}
|
||||
const exec = (tools as Record<string, unknown>).exec;
|
||||
if (exec && typeof exec === "object") {
|
||||
const security = (exec as Record<string, unknown>).security;
|
||||
@@ -592,6 +608,14 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
|
||||
}
|
||||
}
|
||||
}
|
||||
let browserEnabled = true;
|
||||
const browser =
|
||||
"browser" in cfg && cfg.browser && typeof cfg.browser === "object"
|
||||
? (cfg.browser as Record<string, unknown>)
|
||||
: null;
|
||||
if (browser && typeof browser.enabled === "boolean") {
|
||||
browserEnabled = browser.enabled;
|
||||
}
|
||||
let deviceAuth = true;
|
||||
if (gateway) {
|
||||
const controlUi =
|
||||
@@ -602,7 +626,7 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
|
||||
deviceAuth = false;
|
||||
}
|
||||
}
|
||||
return { gatewayAuth, execPolicy, deviceAuth };
|
||||
return { gatewayAuth, execPolicy, deviceAuth, browserEnabled, toolProfile };
|
||||
}
|
||||
|
||||
function resolveQuickSettingsSessionRow(state: AppViewState) {
|
||||
@@ -1100,6 +1124,14 @@ export function renderApp(state: AppViewState) {
|
||||
state.configActiveSection = "auth";
|
||||
requestHostUpdate?.();
|
||||
},
|
||||
onBrowserEnabledToggle: (enabled) => {
|
||||
updateConfigFormValue(state, ["browser", "enabled"], enabled);
|
||||
requestHostUpdate?.();
|
||||
},
|
||||
onToolProfileChange: (profile) => {
|
||||
updateConfigFormValue(state, ["tools", "profile"], profile);
|
||||
requestHostUpdate?.();
|
||||
},
|
||||
theme: state.theme,
|
||||
themeMode: state.themeMode,
|
||||
hasCustomTheme: Boolean(state.settings.customTheme),
|
||||
|
||||
@@ -43,8 +43,12 @@ function createProps(overrides: Partial<QuickSettingsProps> = {}): QuickSettings
|
||||
gatewayAuth: "Unknown",
|
||||
execPolicy: "Allowlist",
|
||||
deviceAuth: true,
|
||||
browserEnabled: true,
|
||||
toolProfile: "coding",
|
||||
},
|
||||
onSecurityConfigure: vi.fn(),
|
||||
onBrowserEnabledToggle: vi.fn(),
|
||||
onToolProfileChange: vi.fn(),
|
||||
theme: "claw",
|
||||
themeMode: "system",
|
||||
hasCustomTheme: false,
|
||||
@@ -109,6 +113,45 @@ describe("renderQuickSettings", () => {
|
||||
expect(container.querySelectorAll(".qs-card--span-all")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("lets operators change browser and tool profile from Security quick settings", () => {
|
||||
const onBrowserEnabledToggle = vi.fn();
|
||||
const onToolProfileChange = vi.fn();
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderQuickSettings(
|
||||
createProps({
|
||||
security: {
|
||||
gatewayAuth: "token",
|
||||
execPolicy: "allowlist",
|
||||
deviceAuth: true,
|
||||
browserEnabled: false,
|
||||
toolProfile: "messaging",
|
||||
},
|
||||
onBrowserEnabledToggle,
|
||||
onToolProfileChange,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const browserInput = Array.from(container.querySelectorAll("input")).find((input) =>
|
||||
input.closest(".qs-row")?.textContent?.includes("Browser enabled"),
|
||||
);
|
||||
expect(browserInput).toBeInstanceOf(HTMLInputElement);
|
||||
expect((browserInput as HTMLInputElement).checked).toBe(false);
|
||||
|
||||
(browserInput as HTMLInputElement).checked = true;
|
||||
browserInput?.dispatchEvent(new Event("change"));
|
||||
expect(onBrowserEnabledToggle).toHaveBeenCalledWith(true);
|
||||
|
||||
expectButtonByText(container, "full").click();
|
||||
expect(onToolProfileChange).toHaveBeenCalledWith("full");
|
||||
expect(expectButtonByText(container, "messaging").classList).toContain(
|
||||
"qs-segmented__btn--active",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the local user name fixed and shows the assistant identity", () => {
|
||||
const container = document.createElement("div");
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ export type QuickSettingsSecurity = {
|
||||
gatewayAuth: string;
|
||||
execPolicy: string;
|
||||
deviceAuth: boolean;
|
||||
browserEnabled: boolean;
|
||||
toolProfile: string;
|
||||
};
|
||||
|
||||
export type QuickSettingsProps = {
|
||||
@@ -71,6 +73,8 @@ export type QuickSettingsProps = {
|
||||
// Security
|
||||
security: QuickSettingsSecurity;
|
||||
onSecurityConfigure?: () => void;
|
||||
onBrowserEnabledToggle?: (enabled: boolean) => void;
|
||||
onToolProfileChange?: (profile: string) => void;
|
||||
|
||||
// Appearance
|
||||
theme: ThemeName;
|
||||
@@ -136,6 +140,7 @@ const BORDER_RADIUS_STOPS: Array<{ value: BorderRadiusStop; label: string }> = [
|
||||
];
|
||||
|
||||
const THINKING_LEVELS = ["off", "low", "medium", "high"];
|
||||
const TOOL_PROFILES = ["minimal", "coding", "messaging", "full"];
|
||||
const LOCAL_USER_LABEL = "You";
|
||||
// Keep raw uploads comfortably below the 2 MB persisted data URL limit after
|
||||
// base64 expansion and a small MIME/header prefix are added.
|
||||
@@ -503,7 +508,11 @@ function renderAutomationsCard(props: QuickSettingsProps) {
|
||||
}
|
||||
|
||||
function renderSecurityCard(props: QuickSettingsProps) {
|
||||
const { gatewayAuth, execPolicy, deviceAuth } = props.security;
|
||||
const { gatewayAuth, execPolicy, deviceAuth, browserEnabled, toolProfile } = props.security;
|
||||
const normalizedToolProfile = toolProfile.trim() || "full";
|
||||
const toolProfiles = TOOL_PROFILES.includes(normalizedToolProfile)
|
||||
? TOOL_PROFILES
|
||||
: [...TOOL_PROFILES, normalizedToolProfile];
|
||||
|
||||
return html`
|
||||
<div class="qs-card qs-card--security">
|
||||
@@ -525,6 +534,37 @@ function renderSecurityCard(props: QuickSettingsProps) {
|
||||
<span class="qs-row__label">Exec policy</span>
|
||||
<span class="qs-row__value"><span class="qs-badge">${execPolicy}</span></span>
|
||||
</div>
|
||||
<div class="qs-row">
|
||||
<span class="qs-row__label">Browser enabled</span>
|
||||
<label class="qs-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${browserEnabled}
|
||||
@change=${(event: Event) =>
|
||||
props.onBrowserEnabledToggle?.((event.currentTarget as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="qs-toggle__track"></span>
|
||||
<span class="qs-toggle__hint muted">${browserEnabled ? "Enabled" : "Disabled"}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="qs-row qs-row--tool-profile">
|
||||
<span class="qs-row__label">Tool profile</span>
|
||||
<div class="qs-segmented">
|
||||
${toolProfiles.map(
|
||||
(profile) => html`
|
||||
<button
|
||||
class="qs-segmented__btn qs-segmented__btn--compact ${profile ===
|
||||
normalizedToolProfile
|
||||
? "qs-segmented__btn--active"
|
||||
: ""}"
|
||||
@click=${() => props.onToolProfileChange?.(profile)}
|
||||
>
|
||||
${profile}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="qs-row">
|
||||
<span class="qs-row__label">Device auth</span>
|
||||
<span class="qs-row__value">
|
||||
|
||||
Reference in New Issue
Block a user