feat(settings): add Ask Questions toggle

This commit is contained in:
Federico Jaramillo Martinez
2026-07-27 12:39:40 +02:00
parent 0e83146315
commit e567d43042
7 changed files with 124 additions and 5 deletions
@@ -0,0 +1,78 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PiWebConfigResponse } from "../../api";
import { SettingsSessiondPanel } from "./SettingsSessiondPanel";
beforeEach(() => {
document.body.replaceChildren();
});
describe("SettingsSessiondPanel Ask Questions setting", () => {
it("lets the user disable ask_user with a daemon config patch", async () => {
const panel = new SettingsSessiondPanel();
const onSave = vi.fn();
panel.configResponse = configResponse(true);
panel.onSave = onSave;
document.body.append(panel);
await panel.updateComplete;
const toggle = askUserToggle(panel);
expect(toggle.checked).toBe(true);
expect(toggle.disabled).toBe(false);
toggle.click();
await Promise.resolve();
expect(onSave).toHaveBeenCalledWith({ askUser: false });
});
it("keeps an environment-overridden setting read-only", async () => {
const panel = new SettingsSessiondPanel();
panel.configResponse = configResponse(true, true);
document.body.append(panel);
await panel.updateComplete;
const toggle = askUserToggle(panel);
expect(toggle.checked).toBe(true);
expect(toggle.disabled).toBe(true);
});
it("does not offer an unsupported setting from an older selected machine", async () => {
const panel = new SettingsSessiondPanel();
panel.configResponse = configResponse(undefined);
document.body.append(panel);
await panel.updateComplete;
const toggle = askUserToggle(panel);
expect(toggle.checked).toBe(false);
expect(toggle.disabled).toBe(true);
});
});
function askUserToggle(panel: SettingsSessiondPanel): HTMLInputElement {
const toggle = panel.shadowRoot?.querySelector<HTMLInputElement>('input[aria-label="Enable Ask Questions"]');
if (toggle === undefined || toggle === null) throw new Error("Ask Questions toggle was not rendered");
return toggle;
}
function configResponse(askUser: boolean | undefined, askUserOverride = false): PiWebConfigResponse {
const askUserConfig = askUser === undefined ? {} : { askUser };
return {
path: "/tmp/pi-web/config.json",
exists: true,
config: askUserConfig,
effectiveConfig: askUserConfig,
envOverrides: {
host: false,
port: false,
allowedHosts: false,
spawnSessions: false,
subsessions: false,
askUser: askUserOverride,
agentCommand: false,
agentDir: false,
agentSessionDir: false,
},
};
}
@@ -5,7 +5,7 @@ import "./SettingsPanelFrame";
import type { SettingsNotice } from "./SettingsPanelFrame";
import { agentProfileConfigPatchFromDraft, agentProfileDraftFromConfig, agentProfileDraftMatchesConfig, emptyAgentProfileConfigDraft, type AgentProfileConfigDraft } from "./settingsConfigDraft";
import type { AgentProfileSettingsSupport } from "./settingsMachineTarget";
import { agentDirFieldOverridden, agentProfileActivationState, spawnSessionsConfigPatch, subsessionsConfigPatch } from "./settingsSessiondConfig";
import { agentDirFieldOverridden, agentProfileActivationState, askUserConfigPatch, spawnSessionsConfigPatch, subsessionsConfigPatch } from "./settingsSessiondConfig";
@customElement("settings-sessiond-panel")
export class SettingsSessiondPanel extends LitElement {
@@ -47,6 +47,11 @@ export class SettingsSessiondPanel extends LitElement {
const subsessionsOverridden = config?.envOverrides.subsessions === true;
// Beta, off by default; also requires spawn to be enabled.
const effectiveSubsessions = config?.effectiveConfig.subsessions === true && effectiveSpawn;
// Current servers always resolve this on-by-default setting. Absence means
// an older selected machine cannot persist it yet.
const askUserSupported = config?.effectiveConfig.askUser !== undefined;
const askUserOverridden = config?.envOverrides.askUser === true;
const effectiveAskUser = config?.effectiveConfig.askUser === true;
const agentCommandOverridden = config?.envOverrides.agentCommand === true;
const profileEditingSupported = this.agentProfileSupport.state === "supported";
const draftCommand = agentCommandOverridden ? (config.effectiveConfig.agent?.command ?? this.agentDraft.command) : this.agentDraft.command;
@@ -141,6 +146,25 @@ export class SettingsSessiondPanel extends LitElement {
</label>
<small>Beta: agents can start child sessions they stay attached to (<code>spawn_subsession</code>, <code>list_subsessions</code>, <code>check_subsession</code>, <code>read_subsession</code>) and are notified when a child finishes. Requires "Allow agents to start sessions". Off by default.</small>
</div>
<div class="field">
<span class="field-heading">
<span>Allow agents to ask questions</span>
${askUserOverridden ? html`<span class="override-badge">environment override</span>` : null}
</span>
<label class="toggle">
<input
type="checkbox"
aria-label="Enable Ask Questions"
.checked=${effectiveAskUser}
?disabled=${this.loading || this.saving || askUserOverridden || !askUserSupported}
@change=${(event: Event) => { void this.toggleAskUser(event); }}
>
<span>Enable the <code>ask_user</code> tool</span>
</label>
<small>${askUserSupported
? html`Agents can post a structured question form and pause until the user responds. On by default.`
: html`This machine does not expose the Ask Questions setting. Update and restart PI WEB on that machine to configure it.`}</small>
</div>
<section class="effective-card" aria-label="Desired and active session daemon configuration summary">
<h3>Desired after environment overrides</h3>
<dl>
@@ -151,6 +175,7 @@ export class SettingsSessiondPanel extends LitElement {
<div><dt>Profile status</dt><dd>${profileActivationLabel(profileActivation)}</dd></div>
<div><dt>Spawn sessions</dt><dd>${effectiveSpawn ? "Enabled" : html`<span class="muted">Disabled</span>`}</dd></div>
<div><dt>Subsessions</dt><dd>${effectiveSubsessions ? "Enabled" : html`<span class="muted">Disabled</span>`}</dd></div>
<div><dt>Ask questions</dt><dd>${!askUserSupported ? html`<span class="muted">Unavailable</span>` : effectiveAskUser ? "Enabled" : html`<span class="muted">Disabled</span>`}</dd></div>
</dl>
</section>
`}
@@ -198,6 +223,11 @@ export class SettingsSessiondPanel extends LitElement {
await this.onSave?.(subsessionsConfigPatch(enabled));
}
private async toggleAskUser(event: Event): Promise<void> {
const enabled = event.target instanceof HTMLInputElement && event.target.checked;
await this.onSave?.(askUserConfigPatch(enabled));
}
static override styles = css`
:host { display: block; }
h3 { margin: 0; font-size: 13px; line-height: 1.3; }
@@ -1,11 +1,12 @@
import { describe, expect, it } from "vitest";
import type { ActiveAgentProfileDescriptor, PiWebConfigResponse, PiWebConfigValues } from "../../api";
import { agentDirFieldOverridden, agentProfileActivationState, mergeSelectedMachineSessiondConfig, spawnSessionsConfigPatch, subsessionsConfigPatch } from "./settingsSessiondConfig";
import { agentDirFieldOverridden, agentProfileActivationState, askUserConfigPatch, mergeSelectedMachineSessiondConfig, spawnSessionsConfigPatch, subsessionsConfigPatch } from "./settingsSessiondConfig";
describe("session daemon settings config helpers", () => {
it("builds daemon-only save patches for the sessiond toggles", () => {
expect(spawnSessionsConfigPatch(false)).toEqual({ spawnSessions: false });
expect(subsessionsConfigPatch(true)).toEqual({ subsessions: true });
expect(askUserConfigPatch(false)).toEqual({ askUser: false });
});
it("compares the desired effective profile with the daemon-owned active profile", () => {
@@ -11,6 +11,10 @@ export function subsessionsConfigPatch(enabled: boolean): PiWebConfigValues {
return { subsessions: enabled };
}
export function askUserConfigPatch(enabled: boolean): PiWebConfigValues {
return { askUser: enabled };
}
export function agentProfileActivationState(
config: PiWebConfigResponse | undefined,
activeProfile: ActiveAgentProfileDescriptor | undefined,