Skip to content

Commit 6213336

Browse files
jbetala7steipete
authored andcommitted
fix(control-ui): show agent thinking defaults
1 parent 9518d1f commit 6213336

10 files changed

Lines changed: 183 additions & 17 deletions

src/gateway/session-utils.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,36 @@ describe("gateway session utils", () => {
14811481
const ops = result.agents.find((agent) => agent.id === "ops");
14821482
expect(ops?.model).toEqual({ primary: "anthropic/claude-opus-4-6" });
14831483
});
1484+
1485+
test("listAgentsForGateway reports per-agent thinking defaults from the agent model", () => {
1486+
const cfg = {
1487+
session: { mainKey: "main" },
1488+
agents: {
1489+
defaults: {
1490+
model: { primary: "minimax/MiniMax-M2.7" },
1491+
thinkingDefault: "off",
1492+
},
1493+
list: [
1494+
{ id: "main", default: true },
1495+
{
1496+
id: "investment-master",
1497+
model: { primary: "deepseek/deepseek-v4-flash" },
1498+
thinkingDefault: "xhigh",
1499+
},
1500+
],
1501+
},
1502+
} as OpenClawConfig;
1503+
1504+
const result = listAgentsForGateway(cfg);
1505+
const agent = result.agents.find((row) => row.id === "investment-master");
1506+
1507+
expect(agent?.model).toEqual({ primary: "deepseek/deepseek-v4-flash" });
1508+
expect(agent?.thinkingDefault).toBe("xhigh");
1509+
expect(agent?.thinkingLevels?.map((level) => level.id)).toEqual(
1510+
expect.arrayContaining(["off", "minimal", "low", "medium", "high", "xhigh"]),
1511+
);
1512+
expect(agent?.thinkingOptions).toEqual(agent?.thinkingLevels?.map((level) => level.label));
1513+
});
14841514
});
14851515

14861516
describe("resolveSessionModelRef", () => {

src/gateway/session-utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,7 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
12191219
const meta = configuredById.get(id);
12201220
const model = resolveGatewayAgentModel(cfg, id);
12211221
const resolvedModel = resolveDefaultModelForAgent({ cfg, agentId: id });
1222+
const thinkingLevels = listThinkingLevelOptions(resolvedModel.provider, resolvedModel.model);
12221223
return Object.assign(
12231224
{
12241225
id,
@@ -1233,6 +1234,14 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
12331234
sessionKey: resolveAgentMainSessionKey({ cfg, agentId: id }),
12341235
acpRuntime: false,
12351236
}),
1237+
thinkingLevels,
1238+
thinkingOptions: thinkingLevels.map((level) => level.label),
1239+
thinkingDefault: resolveGatewaySessionThinkingDefault({
1240+
cfg,
1241+
provider: resolvedModel.provider,
1242+
model: resolvedModel.model,
1243+
agentId: id,
1244+
}),
12361245
},
12371246
model ? { model } : {},
12381247
);

src/gateway/session-utils.types.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { PluginSessionExtensionProjection } from "../plugins/host-hooks.js"
88
import type {
99
GatewayAgentRuntime,
1010
GatewayAgentRow as SharedGatewayAgentRow,
11+
GatewayThinkingLevelOption,
1112
SessionsListResultBase,
1213
SessionsPatchResultBase,
1314
} from "../shared/session-types.js";
@@ -22,11 +23,6 @@ export type GatewaySessionsDefaults = {
2223
thinkingDefault?: string;
2324
};
2425

25-
type GatewayThinkingLevelOption = {
26-
id: string;
27-
label: string;
28-
};
29-
3026
export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeout";
3127

3228
type SubagentRunState = "active" | "interrupted" | "historical";

src/shared/session-types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,21 @@ export type GatewayAgentRuntime = {
1717
source: "env" | "agent" | "defaults" | "model" | "provider" | "implicit" | "session-key";
1818
};
1919

20+
export type GatewayThinkingLevelOption = {
21+
id: string;
22+
label: string;
23+
};
24+
2025
export type GatewayAgentRow = {
2126
id: string;
2227
name?: string;
2328
identity?: GatewayAgentIdentity;
2429
workspace?: string;
2530
model?: GatewayAgentModel;
2631
agentRuntime?: GatewayAgentRuntime;
32+
thinkingLevels?: GatewayThinkingLevelOption[];
33+
thinkingOptions?: string[];
34+
thinkingDefault?: string;
2735
};
2836

2937
export type SessionsListResultBase<TDefaults, TRow> = {

ui/src/ui/chat/session-controls.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,16 @@ function resolveThinkingTargetModel(state: AppViewState): {
862862
};
863863
}
864864

865+
function sessionModelMatchesDefaults(
866+
row: SessionsListResult["sessions"][number] | undefined,
867+
defaults: SessionsListResult["defaults"] | undefined,
868+
): boolean {
869+
return (
870+
(!row?.modelProvider || row.modelProvider === defaults?.modelProvider) &&
871+
(!row?.model || row.model === defaults?.model)
872+
);
873+
}
874+
865875
function buildThinkingOptions(
866876
levels: readonly GatewayThinkingLevelOption[],
867877
currentOverride: string,
@@ -944,17 +954,22 @@ export function resolveChatThinkingSelectState(state: AppViewState): ChatThinkin
944954
typeof persisted === "string" && persisted.trim()
945955
? (normalizeThinkLevel(persisted) ?? persisted.trim())
946956
: "";
957+
const defaults = state.sessionsResult?.defaults;
947958
const { provider, model } = resolveThinkingTargetModel(state);
948959
const levels = resolveThinkingLevelOptions(
949960
activeRow,
950-
state.sessionsResult?.defaults,
961+
defaults,
951962
provider,
952963
model,
953964
state.chatModelCatalog ?? [],
954965
);
966+
const defaultFromSessionDefaults =
967+
(!activeRow || sessionModelMatchesDefaults(activeRow, defaults)) && defaults?.thinkingDefault
968+
? defaults.thinkingDefault
969+
: undefined;
955970
const defaultLevel =
956971
activeRow?.thinkingDefault ??
957-
state.sessionsResult?.defaults?.thinkingDefault ??
972+
defaultFromSessionDefaults ??
958973
(provider && model
959974
? resolveThinkingDefaultForModel({
960975
provider,

ui/src/ui/chat/slash-command-executor.node.test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ describe("executeSlashCommand directives", () => {
657657
);
658658

659659
expect(status.content).toBe(
660-
"Current thinking level: off.\nOptions: default, off, minimal, low, medium, high, xhigh, max.",
660+
"Current thinking level: low.\nOptions: default, off, minimal, low, medium, high, xhigh, max.",
661661
);
662662
expect(setMax.content).toBe("Thinking level set to **max**.");
663663
});
@@ -709,7 +709,44 @@ describe("executeSlashCommand directives", () => {
709709
);
710710

711711
expect(status.content).toBe(
712-
"Current thinking level: high.\nOptions: default, off, minimal, low, medium, high.",
712+
"Current thinking level: low.\nOptions: default, off, minimal, low, medium, high.",
713+
);
714+
});
715+
716+
it("does not report global thinkingDefault for a session with a different model", async () => {
717+
const request = vi.fn(async (method: string, _payload?: unknown) => {
718+
if (method === "sessions.list") {
719+
return {
720+
defaults: {
721+
modelProvider: "minimax",
722+
model: "MiniMax-M2.7",
723+
thinkingDefault: "off",
724+
},
725+
sessions: [
726+
row("agent:main:main", {
727+
modelProvider: "deepseek",
728+
model: "deepseek-v4-flash",
729+
}),
730+
],
731+
};
732+
}
733+
if (method === "models.list") {
734+
return {
735+
models: [{ id: "deepseek-v4-flash", provider: "deepseek", reasoning: true }],
736+
};
737+
}
738+
throw new Error(`unexpected method: ${method}`);
739+
});
740+
741+
const status = await executeSlashCommand(
742+
{ request } as unknown as GatewayBrowserClient,
743+
"agent:main:main",
744+
"think",
745+
"",
746+
);
747+
748+
expect(status.content).toBe(
749+
"Current thinking level: low.\nOptions: default, off, minimal, low, medium, high.",
713750
);
714751
});
715752

ui/src/ui/chat/slash-command-executor.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -587,17 +587,13 @@ function resolveThinkingLevelOptionsForSession(
587587
if (session?.thinkingLevels?.length) {
588588
return session.thinkingLevels;
589589
}
590-
const sessionModelMatchesDefaults =
591-
(!session?.modelProvider || session.modelProvider === defaults?.modelProvider) &&
592-
(!session?.model || session.model === defaults?.model);
593-
if (sessionModelMatchesDefaults && defaults?.thinkingLevels?.length) {
590+
const matchesDefaults = sessionModelMatchesDefaults(session, defaults);
591+
if (matchesDefaults && defaults?.thinkingLevels?.length) {
594592
return defaults.thinkingLevels;
595593
}
596594
const labels =
597595
(session?.thinkingOptions?.length ? session.thinkingOptions : null) ??
598-
(sessionModelMatchesDefaults && defaults?.thinkingOptions?.length
599-
? defaults.thinkingOptions
600-
: null) ??
596+
(matchesDefaults && defaults?.thinkingOptions?.length ? defaults.thinkingOptions : null) ??
601597
formatThinkingLevels(
602598
session?.modelProvider ?? defaults?.modelProvider,
603599
session?.model ?? defaults?.model,
@@ -608,6 +604,16 @@ function resolveThinkingLevelOptionsForSession(
608604
}));
609605
}
610606

607+
function sessionModelMatchesDefaults(
608+
session: GatewaySessionRow | undefined,
609+
defaults: SessionsListResult["defaults"] | undefined,
610+
): boolean {
611+
return (
612+
(!session?.modelProvider || session.modelProvider === defaults?.modelProvider) &&
613+
(!session?.model || session.model === defaults?.model)
614+
);
615+
}
616+
611617
async function loadCurrentSession(
612618
client: GatewayBrowserClient,
613619
sessionKey: string,
@@ -691,7 +697,7 @@ function resolveCurrentThinkingLevel(
691697
if (session?.thinkingDefault) {
692698
return session.thinkingDefault;
693699
}
694-
if (defaults?.thinkingDefault) {
700+
if ((!session || sessionModelMatchesDefaults(session, defaults)) && defaults?.thinkingDefault) {
695701
return defaults.thinkingDefault;
696702
}
697703
const provider = session?.modelProvider ?? defaults?.modelProvider;

ui/src/ui/views/agents-panels-overview.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export function renderAgentOverview(params: {
8383
const skillFilter = Array.isArray(config.entry?.skills) ? config.entry?.skills : null;
8484
const skillCount = skillFilter?.length ?? null;
8585
const disabled = !configForm || configLoading || configSaving;
86+
const thinkingDefault = agent.thinkingDefault ?? "-";
8687

8788
const removeChip = (index: number) => {
8889
const next = fallbackChips.filter((_, i) => i !== index);
@@ -128,6 +129,10 @@ export function renderAgentOverview(params: {
128129
<div class="label">Runtime</div>
129130
<div class="mono">${runtime}</div>
130131
</div>
132+
<div class="agent-kv">
133+
<div class="label">Thinking Default</div>
134+
<div class="mono">${thinkingDefault}</div>
135+
</div>
131136
<div class="agent-kv">
132137
<div class="label">Skills Filter</div>
133138
<div>${skillFilter ? `${skillCount} selected` : "all skills"}</div>

ui/src/ui/views/agents.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,35 @@ describe("renderAgents", () => {
272272
expect(alphaSelect).not.toBe(betaSelect);
273273
});
274274

275+
it("renders the resolved per-agent thinking default in the overview", async () => {
276+
const container = document.createElement("div");
277+
278+
render(
279+
renderAgents(
280+
createProps({
281+
agentsList: {
282+
defaultId: "alpha",
283+
mainKey: "main",
284+
scope: "workspace",
285+
agents: [
286+
{ id: "alpha", name: "Alpha", thinkingDefault: "off" } as never,
287+
{ id: "beta", name: "Beta", thinkingDefault: "xhigh" } as never,
288+
],
289+
},
290+
selectedAgentId: "beta",
291+
}),
292+
),
293+
container,
294+
);
295+
296+
await Promise.resolve();
297+
298+
const thinkingKv = Array.from(container.querySelectorAll(".agent-kv")).find(
299+
(entry) => entry.querySelector(".label")?.textContent?.trim() === "Thinking Default",
300+
);
301+
expect(thinkingKv?.textContent).toContain("xhigh");
302+
});
303+
275304
it("shows the skills count only for the selected agent's report", async () => {
276305
const container = document.createElement("div");
277306
render(

ui/src/ui/views/chat.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,6 +2615,37 @@ describe("chat session controls", () => {
26152615
).toEqual(["Inherited: Off"]);
26162616
});
26172617

2618+
it("does not label a non-default chat model from global thinking defaults", () => {
2619+
const { state } = createChatHeaderState({
2620+
model: "deepseek-v4-flash",
2621+
modelProvider: "deepseek",
2622+
defaultsThinkingDefault: "off",
2623+
models: [
2624+
{
2625+
id: "deepseek-v4-flash",
2626+
name: "DeepSeek V4 Flash",
2627+
provider: "deepseek",
2628+
reasoning: true,
2629+
},
2630+
],
2631+
});
2632+
state.sessionsResult = createSessionsListResult({
2633+
model: "deepseek-v4-flash",
2634+
modelProvider: "deepseek",
2635+
defaultsModel: "MiniMax-M2.7",
2636+
defaultsProvider: "minimax",
2637+
defaultsThinkingDefault: "off",
2638+
});
2639+
const container = document.createElement("div");
2640+
render(renderChatSessionSelect(state), container);
2641+
2642+
const thinkingSelect = container.querySelector<HTMLSelectElement>(
2643+
'select[data-chat-thinking-select="true"]',
2644+
);
2645+
2646+
expect(thinkingSelect?.options[0]?.textContent?.trim()).toBe("Inherited: Low");
2647+
});
2648+
26182649
it("always renders full thinking labels", () => {
26192650
const { state } = createChatHeaderState({
26202651
model: "gpt-5.5",

0 commit comments

Comments
 (0)