fix(ui): resolve session thinking defaults

This commit is contained in:
Val Alexander
2026-05-05 22:11:20 -05:00
parent 307186b76f
commit eba83cc687
4 changed files with 157 additions and 20 deletions

View File

@@ -152,7 +152,7 @@ test("sessions.list uses the gateway model catalog for effective thinking defaul
sessions: expect.arrayContaining([
expect.objectContaining({
key: "agent:main:main",
thinkingDefault: undefined,
thinkingDefault: "medium",
thinkingOptions: ["off", "minimal", "low", "medium", "high"],
}),
]),

View File

@@ -374,6 +374,7 @@ type SessionListRowContext = {
storeChildSessionsByKey: Map<string, string[]>;
selectedModelByOverrideRef: Map<string, ReturnType<typeof resolveSessionModelRef>>;
thinkingLevelsByModelRef: Map<string, ReturnType<typeof listThinkingLevelOptions>>;
thinkingDefaultByAgentModelRef: Map<string, string>;
};
function resolveRuntimeChildSessionKeys(
@@ -492,6 +493,7 @@ function buildSessionListRowContext(params: {
storeChildSessionsByKey: buildStoreChildSessionIndex(params.store, params.now, subagentRuns),
selectedModelByOverrideRef: new Map(),
thinkingLevelsByModelRef: new Map(),
thinkingDefaultByAgentModelRef: new Map(),
};
}
@@ -499,6 +501,17 @@ function createSessionRowModelCacheKey(provider: string | undefined, model: stri
return `${normalizeLowercaseStringOrEmpty(provider)}\0${normalizeOptionalString(model) ?? ""}`;
}
function createSessionRowThinkingDefaultCacheKey(params: {
agentId: string | undefined;
provider: string | undefined;
model: string | undefined;
}) {
return `${normalizeAgentId(params.agentId)}\0${createSessionRowModelCacheKey(
params.provider,
params.model,
)}`;
}
function resolveSessionSelectedModelRef(params: {
cfg: OpenClawConfig;
entry?: SessionEntry;
@@ -548,6 +561,60 @@ function resolveSessionRowThinkingLevels(params: {
return levels;
}
function resolveSessionRowThinkingDefault(params: {
cfg: OpenClawConfig;
provider: string;
model: string;
agentId: string;
modelCatalog?: ModelCatalogEntry[];
rowContext?: SessionListRowContext;
}): string {
if (!params.rowContext) {
return resolveGatewaySessionThinkingDefault({
cfg: params.cfg,
provider: params.provider,
model: params.model,
agentId: params.agentId,
modelCatalog: params.modelCatalog,
});
}
const key = createSessionRowThinkingDefaultCacheKey(params);
if (params.rowContext.thinkingDefaultByAgentModelRef.has(key)) {
return params.rowContext.thinkingDefaultByAgentModelRef.get(key)!;
}
const defaultLevel = resolveGatewaySessionThinkingDefault({
cfg: params.cfg,
provider: params.provider,
model: params.model,
agentId: params.agentId,
modelCatalog: params.modelCatalog,
});
params.rowContext.thinkingDefaultByAgentModelRef.set(key, defaultLevel);
return defaultLevel;
}
function seedSessionRowContextDefaultThinking(params: {
rowContext: SessionListRowContext;
cfg: OpenClawConfig;
defaults: GatewaySessionsDefaults;
}) {
if (
!params.defaults.modelProvider ||
!params.defaults.model ||
!params.defaults.thinkingDefault
) {
return;
}
params.rowContext.thinkingDefaultByAgentModelRef.set(
createSessionRowThinkingDefaultCacheKey({
agentId: resolveDefaultAgentId(params.cfg),
provider: params.defaults.modelProvider,
model: params.defaults.model,
}),
params.defaults.thinkingDefault,
);
}
function mergeChildSessionKeys(
runtimeChildSessions: string[] | undefined,
storeChildSessions: string[] | undefined,
@@ -1702,6 +1769,14 @@ export function buildGatewaySessionRow(params: {
modelCatalog: params.modelCatalog,
rowContext,
});
const thinkingDefault = resolveSessionRowThinkingDefault({
cfg,
provider: thinkingProvider,
model: thinkingModel,
agentId: sessionAgentId,
modelCatalog: params.modelCatalog,
rowContext,
});
const pluginExtensions =
!lightweight && entry ? projectPluginSessionExtensionsSync({ sessionKey: key, entry }) : [];
@@ -1731,15 +1806,7 @@ export function buildGatewaySessionRow(params: {
thinkingLevel: entry?.thinkingLevel,
thinkingLevels,
thinkingOptions: thinkingLevels.map((level) => level.label),
thinkingDefault: lightweight
? entry?.thinkingLevel
: resolveGatewaySessionThinkingDefault({
cfg,
provider: thinkingProvider,
model: thinkingModel,
agentId: sessionAgentId,
modelCatalog: params.modelCatalog,
}),
thinkingDefault,
fastMode: entry?.fastMode,
verboseLevel: entry?.verboseLevel,
traceLevel: entry?.traceLevel,
@@ -2040,6 +2107,10 @@ export function listSessionsFromStore(params: {
defaultLimit: SESSIONS_LIST_DEFAULT_LIMIT,
});
const { entries, totalCount, limitApplied } = selection;
const defaults = getSessionDefaults(cfg, params.modelCatalog);
if (entries.length > 0) {
seedSessionRowContextDefaultThinking({ rowContext: getRowContext(), cfg, defaults });
}
const sessions = entries.map(([key, entry], index) => {
const includeTranscriptFields = index < sessionListTranscriptFieldRows;
@@ -2066,7 +2137,7 @@ export function listSessionsFromStore(params: {
totalCount,
limitApplied,
hasMore: sessions.length < totalCount,
defaults: getSessionDefaults(cfg, params.modelCatalog),
defaults,
sessions,
};
}
@@ -2109,6 +2180,10 @@ export async function listSessionsFromStoreAsync(params: {
defaultLimit: SESSIONS_LIST_DEFAULT_LIMIT,
});
const { entries, totalCount, limitApplied } = selection;
const defaults = getSessionDefaults(cfg, params.modelCatalog);
if (entries.length > 0) {
seedSessionRowContextDefaultThinking({ rowContext: getRowContext(), cfg, defaults });
}
const sessions: GatewaySessionRow[] = [];
for (let i = 0; i < entries.length; i++) {
@@ -2167,7 +2242,7 @@ export async function listSessionsFromStoreAsync(params: {
totalCount,
limitApplied,
hasMore: sessions.length < totalCount,
defaults: getSessionDefaults(cfg, params.modelCatalog),
defaults,
sessions,
};
}

View File

@@ -5,12 +5,15 @@ import { describe, expect, it, vi } from "vitest";
import type { SessionsListResult } from "../types.ts";
import { renderSessions, type SessionsProps } from "./sessions.ts";
function buildResult(session: SessionsListResult["sessions"][number]): SessionsListResult {
function buildResult(
session: SessionsListResult["sessions"][number],
defaults?: Partial<SessionsListResult["defaults"]>,
): SessionsListResult {
return {
ts: Date.now(),
path: "(multiple)",
count: 1,
defaults: { modelProvider: null, model: null, contextTokens: null },
defaults: { modelProvider: null, model: null, contextTokens: null, ...defaults },
sessions: [session],
};
}
@@ -268,6 +271,41 @@ describe("sessions view", () => {
).toBe("Override: adaptive");
});
it("labels inherited thinking from list defaults when lightweight rows omit row defaults", async () => {
const container = document.createElement("div");
render(
renderSessions(
buildProps(
buildResult(
{
key: "agent:main:main",
kind: "direct",
updatedAt: Date.now(),
},
{
modelProvider: "openai-codex",
model: "gpt-5.5",
thinkingDefault: "high",
thinkingLevels: [
{ id: "off", label: "off" },
{ id: "high", label: "high" },
],
},
),
),
),
container,
);
await Promise.resolve();
const thinking = container.querySelector("tbody select") as HTMLSelectElement | null;
expect(thinking?.value).toBe("");
expect(thinking?.options[0]?.textContent?.trim()).toBe("Inherited: high");
expect(Array.from(thinking?.options ?? []).map((option) => option.textContent?.trim())).toEqual(
["Inherited: high", "Off", "Override: high"],
);
});
it("keeps legacy binary thinking labels patching canonical ids", async () => {
const container = document.createElement("div");
const onPatch = vi.fn();

View File

@@ -92,16 +92,37 @@ function getAgentIdentity(
: null;
}
function rowMatchesSessionDefaults(
row: GatewaySessionRow,
defaults: SessionsListResult["defaults"] | undefined,
): boolean {
return (
(!row.modelProvider || row.modelProvider === defaults?.modelProvider) &&
(!row.model || row.model === defaults?.model)
);
}
function resolveThinkLevelOptions(
row: GatewaySessionRow,
defaults?: SessionsListResult["defaults"],
): readonly { value: string; label: string }[] {
const defaultLabel = formatInheritedThinkingLabel(row.thinkingDefault);
const sessionModelMatchesDefaults = rowMatchesSessionDefaults(row, defaults);
const defaultLabel = formatInheritedThinkingLabel(
row.thinkingDefault ?? (sessionModelMatchesDefaults ? defaults?.thinkingDefault : undefined),
);
const options: readonly GatewayThinkingLevelOption[] = row.thinkingLevels?.length
? row.thinkingLevels
: (row.thinkingOptions?.length ? row.thinkingOptions : DEFAULT_THINK_LEVELS).map((label) => ({
id: normalizeThinkingOptionValue(label),
label,
}));
: sessionModelMatchesDefaults && defaults?.thinkingLevels?.length
? defaults.thinkingLevels
: (row.thinkingOptions?.length
? row.thinkingOptions
: sessionModelMatchesDefaults && defaults?.thinkingOptions?.length
? defaults.thinkingOptions
: DEFAULT_THINK_LEVELS
).map((label) => ({
id: normalizeThinkingOptionValue(label),
label,
}));
return [
{ value: "", label: defaultLabel },
...options.map((option) => ({
@@ -617,7 +638,10 @@ function renderRows(row: GatewaySessionRow, props: SessionsProps) {
const updated = row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : t("common.na");
const rawThinking = row.thinkingLevel ?? "";
const thinking = rawThinking ? normalizeThinkingOptionValue(rawThinking) : "";
const thinkLevels = withCurrentLabeledOption(resolveThinkLevelOptions(row), thinking);
const thinkLevels = withCurrentLabeledOption(
resolveThinkLevelOptions(row, props.result?.defaults),
thinking,
);
const fastMode = row.fastMode === true ? "on" : row.fastMode === false ? "off" : "";
const fastLevels = withCurrentLabeledOption(buildFastLevelOptions(), fastMode);
const verbose = row.verboseLevel ?? "";