Skip to content

Commit 06adff1

Browse files
committed
Add browser controls to quick settings
1 parent 5a9e8a2 commit 06adff1

7 files changed

Lines changed: 175 additions & 12 deletions

File tree

122 KB
Loading

ui/src/styles/config-quick.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,23 @@
214214
color: var(--accent);
215215
}
216216

217+
.qs-row--tool-profile {
218+
align-items: stretch;
219+
flex-direction: column;
220+
gap: 8px;
221+
padding-block: 10px;
222+
}
223+
224+
.qs-row--tool-profile .qs-row__label {
225+
flex: 0 0 auto;
226+
width: 100%;
227+
}
228+
229+
.qs-row--tool-profile .qs-segmented {
230+
align-self: stretch;
231+
justify-content: flex-start;
232+
}
233+
217234
.qs-field {
218235
display: grid;
219236
gap: 6px;

ui/src/ui/app-render.assistant-avatar.test.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,17 +241,29 @@ describe("renderApp assistant avatar routing", () => {
241241
expect(shell?.style.getPropertyValue("--chat-message-max-width")).toBe("min(1280px, 82%)");
242242
});
243243

244-
it("passes tools.exec.security to Quick Settings", () => {
245-
renderApp(
246-
createState({
247-
configForm: {
248-
tools: { exec: { security: "full" } },
249-
agents: { defaults: { exec: { security: "deny" } } },
250-
},
251-
}),
252-
);
244+
it("passes security quick setting fields to Quick Settings", () => {
245+
const state = createState({
246+
configForm: {
247+
browser: { enabled: false },
248+
tools: { profile: "messaging", exec: { security: "full" } },
249+
agents: { defaults: { exec: { security: "deny" } } },
250+
},
251+
});
252+
253+
renderApp(state);
253254

254255
expect(quickSettingsProps.current?.security.execPolicy).toBe("full");
256+
expect(quickSettingsProps.current?.security.browserEnabled).toBe(false);
257+
expect(quickSettingsProps.current?.security.toolProfile).toBe("messaging");
258+
259+
quickSettingsProps.current?.onBrowserEnabledToggle?.(true);
260+
quickSettingsProps.current?.onToolProfileChange?.("full");
261+
262+
expect(state.configForm?.browser).toEqual({ enabled: true });
263+
expect(state.configForm?.tools).toMatchObject({
264+
profile: "full",
265+
exec: { security: "full" },
266+
});
255267
});
256268

257269
it("renders stale cron state containing a job without a payload", () => {

ui/src/ui/app-render.exec-policy.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ describe("extractQuickSettingsSecurity", () => {
3131
);
3232
});
3333

34+
it("reads browser enabled and tool profile from canonical config paths", () => {
35+
const result = extractQuickSettingsSecurity(
36+
makeState({ browser: { enabled: false }, tools: { profile: "messaging" } }),
37+
);
38+
39+
expect(result.browserEnabled).toBe(false);
40+
expect(result.toolProfile).toBe("messaging");
41+
});
42+
43+
it("uses effective quick settings defaults when browser and tool profile are unset", () => {
44+
const result = extractQuickSettingsSecurity(makeState({}));
45+
46+
expect(result.browserEnabled).toBe(true);
47+
expect(result.toolProfile).toBe("full");
48+
});
49+
3450
it("ignores agents.defaults.exec.security because it is not a schema path", () => {
3551
const result = extractQuickSettingsSecurity(
3652
makeState({
@@ -58,5 +74,8 @@ describe("extractQuickSettingsSecurity", () => {
5874
expect(
5975
extractQuickSettingsSecurity(makeState({ tools: { exec: { security: " " } } })).execPolicy,
6076
).toBe("allowlist");
77+
expect(
78+
extractQuickSettingsSecurity(makeState({ tools: { profile: " coding " } })).toolProfile,
79+
).toBe("coding");
6180
});
6281
});

ui/src/ui/app-render.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,10 +549,18 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
549549
gatewayAuth: string;
550550
execPolicy: string;
551551
deviceAuth: boolean;
552+
browserEnabled: boolean;
553+
toolProfile: string;
552554
} {
553555
const config = state.configForm ?? state.configSnapshot?.config;
554556
if (!config || typeof config !== "object") {
555-
return { gatewayAuth: "unknown", execPolicy: "unknown", deviceAuth: false };
557+
return {
558+
gatewayAuth: "unknown",
559+
execPolicy: "unknown",
560+
deviceAuth: false,
561+
browserEnabled: true,
562+
toolProfile: "full",
563+
};
556564
}
557565
const cfg = config;
558566
const gateway =
@@ -579,8 +587,16 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
579587
}
580588
}
581589
let execPolicy = "allowlist";
590+
let toolProfile = "full";
582591
const tools = cfg.tools;
583592
if (tools && typeof tools === "object") {
593+
const profile = (tools as Record<string, unknown>).profile;
594+
if (typeof profile === "string") {
595+
const trimmedProfile = profile.trim();
596+
if (trimmedProfile) {
597+
toolProfile = trimmedProfile;
598+
}
599+
}
584600
const exec = (tools as Record<string, unknown>).exec;
585601
if (exec && typeof exec === "object") {
586602
const security = (exec as Record<string, unknown>).security;
@@ -592,6 +608,14 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
592608
}
593609
}
594610
}
611+
let browserEnabled = true;
612+
const browser =
613+
"browser" in cfg && cfg.browser && typeof cfg.browser === "object"
614+
? (cfg.browser as Record<string, unknown>)
615+
: null;
616+
if (browser && typeof browser.enabled === "boolean") {
617+
browserEnabled = browser.enabled;
618+
}
595619
let deviceAuth = true;
596620
if (gateway) {
597621
const controlUi =
@@ -602,7 +626,7 @@ export function extractQuickSettingsSecurity(state: AppViewState): {
602626
deviceAuth = false;
603627
}
604628
}
605-
return { gatewayAuth, execPolicy, deviceAuth };
629+
return { gatewayAuth, execPolicy, deviceAuth, browserEnabled, toolProfile };
606630
}
607631

608632
function resolveQuickSettingsSessionRow(state: AppViewState) {
@@ -1100,6 +1124,14 @@ export function renderApp(state: AppViewState) {
11001124
state.configActiveSection = "auth";
11011125
requestHostUpdate?.();
11021126
},
1127+
onBrowserEnabledToggle: (enabled) => {
1128+
updateConfigFormValue(state, ["browser", "enabled"], enabled);
1129+
requestHostUpdate?.();
1130+
},
1131+
onToolProfileChange: (profile) => {
1132+
updateConfigFormValue(state, ["tools", "profile"], profile);
1133+
requestHostUpdate?.();
1134+
},
11031135
theme: state.theme,
11041136
themeMode: state.themeMode,
11051137
hasCustomTheme: Boolean(state.settings.customTheme),

ui/src/ui/views/config-quick.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ function createProps(overrides: Partial<QuickSettingsProps> = {}): QuickSettings
4343
gatewayAuth: "Unknown",
4444
execPolicy: "Allowlist",
4545
deviceAuth: true,
46+
browserEnabled: true,
47+
toolProfile: "coding",
4648
},
4749
onSecurityConfigure: vi.fn(),
50+
onBrowserEnabledToggle: vi.fn(),
51+
onToolProfileChange: vi.fn(),
4852
theme: "claw",
4953
themeMode: "system",
5054
hasCustomTheme: false,
@@ -109,6 +113,45 @@ describe("renderQuickSettings", () => {
109113
expect(container.querySelectorAll(".qs-card--span-all")).toHaveLength(1);
110114
});
111115

116+
it("lets operators change browser and tool profile from Security quick settings", () => {
117+
const onBrowserEnabledToggle = vi.fn();
118+
const onToolProfileChange = vi.fn();
119+
const container = document.createElement("div");
120+
121+
render(
122+
renderQuickSettings(
123+
createProps({
124+
security: {
125+
gatewayAuth: "token",
126+
execPolicy: "allowlist",
127+
deviceAuth: true,
128+
browserEnabled: false,
129+
toolProfile: "messaging",
130+
},
131+
onBrowserEnabledToggle,
132+
onToolProfileChange,
133+
}),
134+
),
135+
container,
136+
);
137+
138+
const browserInput = Array.from(container.querySelectorAll("input")).find((input) =>
139+
input.closest(".qs-row")?.textContent?.includes("Browser enabled"),
140+
);
141+
expect(browserInput).toBeInstanceOf(HTMLInputElement);
142+
expect((browserInput as HTMLInputElement).checked).toBe(false);
143+
144+
(browserInput as HTMLInputElement).checked = true;
145+
browserInput?.dispatchEvent(new Event("change"));
146+
expect(onBrowserEnabledToggle).toHaveBeenCalledWith(true);
147+
148+
expectButtonByText(container, "full").click();
149+
expect(onToolProfileChange).toHaveBeenCalledWith("full");
150+
expect(expectButtonByText(container, "messaging").classList).toContain(
151+
"qs-segmented__btn--active",
152+
);
153+
});
154+
112155
it("keeps the local user name fixed and shows the assistant identity", () => {
113156
const container = document.createElement("div");
114157

ui/src/ui/views/config-quick.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export type QuickSettingsSecurity = {
4747
gatewayAuth: string;
4848
execPolicy: string;
4949
deviceAuth: boolean;
50+
browserEnabled: boolean;
51+
toolProfile: string;
5052
};
5153

5254
export type QuickSettingsProps = {
@@ -71,6 +73,8 @@ export type QuickSettingsProps = {
7173
// Security
7274
security: QuickSettingsSecurity;
7375
onSecurityConfigure?: () => void;
76+
onBrowserEnabledToggle?: (enabled: boolean) => void;
77+
onToolProfileChange?: (profile: string) => void;
7478

7579
// Appearance
7680
theme: ThemeName;
@@ -136,6 +140,7 @@ const BORDER_RADIUS_STOPS: Array<{ value: BorderRadiusStop; label: string }> = [
136140
];
137141

138142
const THINKING_LEVELS = ["off", "low", "medium", "high"];
143+
const TOOL_PROFILES = ["minimal", "coding", "messaging", "full"];
139144
const LOCAL_USER_LABEL = "You";
140145
// Keep raw uploads comfortably below the 2 MB persisted data URL limit after
141146
// base64 expansion and a small MIME/header prefix are added.
@@ -503,7 +508,11 @@ function renderAutomationsCard(props: QuickSettingsProps) {
503508
}
504509

505510
function renderSecurityCard(props: QuickSettingsProps) {
506-
const { gatewayAuth, execPolicy, deviceAuth } = props.security;
511+
const { gatewayAuth, execPolicy, deviceAuth, browserEnabled, toolProfile } = props.security;
512+
const normalizedToolProfile = toolProfile.trim() || "full";
513+
const toolProfiles = TOOL_PROFILES.includes(normalizedToolProfile)
514+
? TOOL_PROFILES
515+
: [...TOOL_PROFILES, normalizedToolProfile];
507516

508517
return html`
509518
<div class="qs-card qs-card--security">
@@ -525,6 +534,37 @@ function renderSecurityCard(props: QuickSettingsProps) {
525534
<span class="qs-row__label">Exec policy</span>
526535
<span class="qs-row__value"><span class="qs-badge">${execPolicy}</span></span>
527536
</div>
537+
<div class="qs-row">
538+
<span class="qs-row__label">Browser enabled</span>
539+
<label class="qs-toggle">
540+
<input
541+
type="checkbox"
542+
.checked=${browserEnabled}
543+
@change=${(event: Event) =>
544+
props.onBrowserEnabledToggle?.((event.currentTarget as HTMLInputElement).checked)}
545+
/>
546+
<span class="qs-toggle__track"></span>
547+
<span class="qs-toggle__hint muted">${browserEnabled ? "Enabled" : "Disabled"}</span>
548+
</label>
549+
</div>
550+
<div class="qs-row qs-row--tool-profile">
551+
<span class="qs-row__label">Tool profile</span>
552+
<div class="qs-segmented">
553+
${toolProfiles.map(
554+
(profile) => html`
555+
<button
556+
class="qs-segmented__btn qs-segmented__btn--compact ${profile ===
557+
normalizedToolProfile
558+
? "qs-segmented__btn--active"
559+
: ""}"
560+
@click=${() => props.onToolProfileChange?.(profile)}
561+
>
562+
${profile}
563+
</button>
564+
`,
565+
)}
566+
</div>
567+
</div>
528568
<div class="qs-row">
529569
<span class="qs-row__label">Device auth</span>
530570
<span class="qs-row__value">

0 commit comments

Comments
 (0)