diff --git a/plugins/admin/public/index.html b/plugins/admin/public/index.html
index 7096c9ad8..e6b3d6f7e 100644
--- a/plugins/admin/public/index.html
+++ b/plugins/admin/public/index.html
@@ -4246,6 +4246,32 @@
The models, browser runtime, and organization context available on future turns.
+
Default harness and model
@@ -6212,6 +6238,9 @@ Confirm governance change
const showOrgAmbient = scope.startsWith("org:") && "orgAmbient" in r.data;
$("card-org-ambient").classList.toggle("hidden", !showOrgAmbient);
if (showOrgAmbient) $("org-ambient").checked = r.data.orgAmbient !== false;
+ const showInteractiveFastMode = scope.startsWith("org:") && "interactiveFastMode" in r.data;
+ $("card-interactive-fast-mode").classList.toggle("hidden", !showInteractiveFastMode);
+ if (showInteractiveFastMode) $("interactive-fast-mode").checked = r.data.interactiveFastMode === true;
const opts = r.data.baseModelOptions || [];
const dflt = r.data.baseModelDefault || "";
@@ -7694,6 +7723,7 @@ Confirm governance change
egress: () => collectEgress(),
"external-slack-participants": () => ({ on: $("external-slack-participants").checked }),
"org-ambient": () => ({ on: $("org-ambient").checked }),
+ "interactive-fast-mode": () => ({ on: $("interactive-fast-mode").checked }),
runtime: () => ({ harnessId: $("base-harness").value, modelId: $("base-model").value }),
"approved-harnesses": () => ({
ids: Array.from(document.querySelectorAll("#approved-harnesses-list input[type=checkbox]:checked")).map(
@@ -7727,6 +7757,7 @@ Confirm governance change
egress: "st-egress",
"external-slack-participants": "st-external-slack-participants",
"org-ambient": "st-org-ambient",
+ "interactive-fast-mode": "st-interactive-fast-mode",
runtime: "st-runtime",
"approved-harnesses": "st-approved-harnesses",
"webui-models": "st-webui-models",
diff --git a/plugins/web-ui/src/chat.ts b/plugins/web-ui/src/chat.ts
index 170e67350..44a3c1826 100644
--- a/plugins/web-ui/src/chat.ts
+++ b/plugins/web-ui/src/chat.ts
@@ -346,7 +346,9 @@ function currentTurnOptions(): TurnOptions {
const { harnessId: harness } = currentModelOption();
return {
...(harnessSupportsEffort(harness) ? { effortLevel: composerState.effortLevel } : {}),
- ...(harnessSupportsFastMode(harness) ? { fastMode: composerState.fastMode } : {}),
+ ...(harnessSupportsFastMode(harness) && typeof composerState.fastMode === "boolean"
+ ? { fastMode: composerState.fastMode }
+ : {}),
harness,
scopeId: chatState.scopeId,
channelName: chatState.contextName,
diff --git a/plugins/web-ui/src/composer.ts b/plugins/web-ui/src/composer.ts
index 51080c2c5..03fe4fe5a 100644
--- a/plugins/web-ui/src/composer.ts
+++ b/plugins/web-ui/src/composer.ts
@@ -118,7 +118,7 @@ function modelOptionFor(value: ModelOptionValue): ModelOption {
);
}
-function loadStoredFastMode(): boolean {
+function loadStoredFastMode(): boolean | undefined {
try {
const stored = localStorage.getItem(FAST_MODE_STORAGE_KEY);
if (stored === "0") return false;
@@ -126,7 +126,7 @@ function loadStoredFastMode(): boolean {
} catch {
void 0;
}
- return true;
+ return undefined;
}
function loadStoredEffort(fallback: EffortLevel): EffortLevel {
@@ -209,6 +209,11 @@ let dragDepth = 0;
let skillsLoading = false;
let slashActiveIndex = 0;
let fastModeCharging = false;
+let orgFastModeDefault = false;
+
+function effectiveFastMode(): boolean {
+ return composerState.fastMode ?? orgFastModeDefault;
+}
let fastModeChargeTimer: ReturnType | null = null;
export function resetComposer(): void {
@@ -242,6 +247,7 @@ export async function refreshRuntimeSelection(scopeId: string | null, agent?: Ag
}
activeRuntimeConfig = config;
setFastModeModelIds(config.fastModeModelIds);
+ orgFastModeDefault = config.interactiveFastMode === true;
applyRuntimeOptions(config.approvedHarnesses, config.modelsByHarness, config.effective, config.modelCatalog);
if (agent && (!chatState.threadRef || !threadModelPicks.has(chatState.threadRef)))
agent.state.model = currentModelOption().model;
@@ -260,6 +266,7 @@ async function changeScopeRuntime(
if (request !== runtimeRequest || scopeId !== chatState.scopeId) return;
activeRuntimeConfig = config;
setFastModeModelIds(config.fastModeModelIds);
+ orgFastModeDefault = config.interactiveFastMode === true;
applyRuntimeOptions(config.approvedHarnesses, config.modelsByHarness, config.effective, config.modelCatalog);
if (!chatState.threadRef || !threadModelPicks.has(chatState.threadRef))
agent.state.model = currentModelOption().model;
@@ -285,7 +292,7 @@ export function composerForm(agent: Agent): TemplateResult {
const effortAvailable = harnessSupportsEffort(selectedModel.harnessId);
const fastSupported = harnessSupportsFastMode(selectedModel.harnessId);
const fastAvailable = fastSupported && modelSupportsFastMode(selectedModel.model.id);
- const fastOn = fastAvailable && composerState.fastMode;
+ const fastOn = fastAvailable && effectiveFastMode();
const fastCharging = fastModeCharging && fastOn;
let fastTitle = "Fast mode is only available on Opus models";
if (fastAvailable) fastTitle = fastOn ? "Fast mode active" : "Fast mode";
@@ -676,7 +683,7 @@ function composerApprovalPanel(approvals: PendingApproval[]): TemplateResult {
function settingsControl(agent: Agent, selected: ModelOption, disabled: boolean): TemplateResult {
const open = composerState.openMenu === "settings";
const fastAvailable = harnessSupportsFastMode(selected.harnessId) && modelSupportsFastMode(selected.model.id);
- const fastOn = fastAvailable && composerState.fastMode;
+ const fastOn = fastAvailable && effectiveFastMode();
const summary = `${selected.buttonLabel} · ${effortLabel(composerState.effortLevel)}${fastOn ? " · Fast" : ""}`;
return html`