diff --git a/CHANGELOG.md b/CHANGELOG.md index fddb10e8a59..51e0d343f12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ Docs: https://docs.openclaw.ai - Plugins/catalog: add an `@tencent-weixin/openclaw-weixin` external entry pinned to `2.4.1` so onboarding and `openclaw channels add` can install the Tencent Weixin (personal WeChat) channel by default. (#77269) Thanks @pumpkinxing1. - Developer tooling: add checked-in VS Code Gateway debugging configs and an opt-in `OUTPUT_SOURCE_MAPS=1` source-map build path for breakpoints in TypeScript source. (#45710) Thanks @SwissArmyBud. - Managed proxy: add `proxy.loopbackMode` for Gateway loopback control-plane traffic, allowing operators to keep the default Gateway loopback bypass, force loopback Gateway traffic through the proxy, or block it. (#77018) Thanks @jesse-merhi. +- Telegram/native commands: show the current thinking level above the `/think` level picker so users can see the active setting before changing it. (#78278) Thanks @obviyus. ### Fixes diff --git a/extensions/telegram/src/bot-native-commands.session-meta.test.ts b/extensions/telegram/src/bot-native-commands.session-meta.test.ts index c33927d4669..3469b8eb441 100644 --- a/extensions/telegram/src/bot-native-commands.session-meta.test.ts +++ b/extensions/telegram/src/bot-native-commands.session-meta.test.ts @@ -512,12 +512,24 @@ describe("registerTelegramNativeCommands — session metadata", () => { }); it("uses the target session model when building native argument menus", async () => { - const cfg: OpenClawConfig = {}; + const cfg = { + agents: { + defaults: { + thinkingDefault: "low", + models: { + "anthropic/claude-opus-4-7": { + params: { thinking: "xhigh" }, + }, + }, + }, + }, + } as OpenClawConfig; sessionMocks.loadSessionStore.mockReturnValue({ "agent:main:main": { providerOverride: "anthropic", modelOverride: "claude-opus-4-7", modelOverrideSource: "user", + thinkingLevel: "high", updatedAt: 0, }, }); @@ -541,7 +553,7 @@ describe("registerTelegramNativeCommands — session metadata", () => { expect(sessionMocks.loadSessionStore).toHaveBeenCalledWith("/tmp/openclaw-sessions.json"); expect(sendMessage).toHaveBeenCalledWith( 100, - expect.stringContaining("Choose level for /think."), + expect.stringContaining("Current thinking level: high.\nChoose level for /think."), expect.objectContaining({ reply_markup: expect.any(Object) }), ); expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled(); @@ -587,6 +599,7 @@ describe("registerTelegramNativeCommands — session metadata", () => { agents: { defaults: { model: { primary: "openai/gpt-5.5" }, + thinkingDefault: "medium", }, }, } as OpenClawConfig; @@ -619,7 +632,81 @@ describe("registerTelegramNativeCommands — session metadata", () => { ); expect(sendMessage).toHaveBeenCalledWith( 100, - expect.stringContaining("Choose level for /think."), + expect.stringContaining("Current thinking level: medium.\nChoose level for /think."), + expect.objectContaining({ reply_markup: expect.any(Object) }), + ); + expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled(); + }); + + it("uses target model thinking defaults before global thinking defaults", async () => { + const cfg = { + agents: { + defaults: { + thinkingDefault: "low", + models: { + "anthropic/claude-opus-4-7": { + params: { thinking: "xhigh" }, + }, + }, + }, + }, + } as OpenClawConfig; + sessionMocks.loadSessionStore.mockReturnValue({ + "agent:main:main": { + providerOverride: "anthropic", + modelOverride: "claude-opus-4-7", + modelOverrideSource: "user", + updatedAt: 0, + }, + }); + + const { handler, sendMessage } = registerAndResolveCommandHandler({ + commandName: "think", + cfg, + allowFrom: ["*"], + }); + await handler(createTelegramPrivateCommandContext()); + + expect(sendMessage).toHaveBeenCalledWith( + 100, + expect.stringContaining("Current thinking level: xhigh.\nChoose level for /think."), + expect.objectContaining({ reply_markup: expect.any(Object) }), + ); + expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled(); + }); + + it("uses per-agent thinking defaults before target model and global thinking defaults", async () => { + const cfg = { + agents: { + defaults: { + thinkingDefault: "low", + models: { + "anthropic/claude-opus-4-7": { + params: { thinking: "xhigh" }, + }, + }, + }, + list: [ + { + id: "alpha", + model: { primary: "anthropic/claude-opus-4-7" }, + thinkingDefault: "minimal", + }, + ], + }, + } as OpenClawConfig; + sessionMocks.loadSessionStore.mockReturnValue({}); + + const { handler, sendMessage } = registerAndResolveCommandHandler({ + commandName: "think", + cfg, + allowFrom: ["*"], + }); + await handler(createTelegramPrivateCommandContext()); + + expect(sendMessage).toHaveBeenCalledWith( + 100, + expect.stringContaining("Current thinking level: minimal.\nChoose level for /think."), expect.objectContaining({ reply_markup: expect.any(Object) }), ); expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled(); diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 2d2fb37cb22..00622c15603 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -1,7 +1,12 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; import type { Bot, Context } from "grammy"; -import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime"; +import { + buildConfiguredModelCatalog, + resolveAgentConfig, + resolveDefaultModelForAgent, + resolveThinkingDefault, +} from "openclaw/plugin-sdk/agent-runtime"; import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-streaming"; import { resolveCommandAuthorization, @@ -203,7 +208,7 @@ function resolveTelegramCommandMenuModelContext(params: { cfg: OpenClawConfig; agentId: string; sessionKey: string; -}): { provider?: string; model?: string } { +}): { provider?: string; model?: string; thinkingLevel?: string } { if (!params.sessionKey.trim()) { return {}; } @@ -215,8 +220,13 @@ function resolveTelegramCommandMenuModelContext(params: { }); const store = loadSessionStore(storePath); const entry = resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing; + const thinkingLevel = normalizeOptionalString(entry?.thinkingLevel); if (entry?.modelOverrideSource === "auto" && normalizeOptionalString(entry.modelOverride)) { - return { provider: defaultModel.provider, model: defaultModel.model }; + return { + provider: defaultModel.provider, + model: defaultModel.model, + ...(thinkingLevel ? { thinkingLevel } : {}), + }; } const override = resolveStoredModelOverride({ sessionEntry: entry, @@ -228,6 +238,7 @@ function resolveTelegramCommandMenuModelContext(params: { return { provider: override.provider || defaultModel.provider, model: override.model, + ...(thinkingLevel ? { thinkingLevel } : {}), }; } const provider = @@ -238,12 +249,54 @@ function resolveTelegramCommandMenuModelContext(params: { return { ...(provider ? { provider } : {}), ...(model ? { model } : {}), + ...(thinkingLevel ? { thinkingLevel } : {}), }; } catch { return {}; } } +function resolveTelegramThinkMenuCurrentLevel(params: { + cfg: OpenClawConfig; + agentId: string; + provider?: string; + model?: string; + thinkingLevel?: string; +}): string { + const explicit = normalizeOptionalString(params.thinkingLevel); + if (explicit) { + return explicit; + } + const agentThinkingDefault = normalizeOptionalString( + resolveAgentConfig(params.cfg, params.agentId)?.thinkingDefault, + ); + if (agentThinkingDefault) { + return agentThinkingDefault; + } + const defaultModel = resolveDefaultModelForAgent({ + cfg: params.cfg, + agentId: params.agentId, + }); + return resolveThinkingDefault({ + cfg: params.cfg, + provider: params.provider ?? defaultModel.provider, + model: params.model ?? defaultModel.model, + catalog: buildConfiguredModelCatalog({ cfg: params.cfg }), + }); +} + +function formatTelegramCommandArgMenuTitle(params: { + command: NonNullable>; + menu: NonNullable>; + currentThinkingLevel?: string; +}): string { + const title = formatCommandArgMenuTitle({ command: params.command, menu: params.menu }); + if (params.command.key !== "think" || !params.currentThinkingLevel) { + return title; + } + return `Current thinking level: ${params.currentThinkingLevel}.\n${title}`; +} + function resolveTelegramNativeReplyChannelData( result: TelegramNativeReplyPayload, ): TelegramNativeReplyChannelData | undefined { @@ -1006,7 +1059,18 @@ export const registerTelegramNativeCommands = ({ }) : null; if (menu && commandDefinition) { - const title = formatCommandArgMenuTitle({ command: commandDefinition, menu }); + const title = formatTelegramCommandArgMenuTitle({ + command: commandDefinition, + menu, + currentThinkingLevel: + commandDefinition.key === "think" + ? resolveTelegramThinkMenuCurrentLevel({ + cfg: runtimeCfg, + agentId: route.agentId, + ...menuModelContext, + }) + : undefined, + }); const rows: Array> = []; for (let i = 0; i < menu.choices.length; i += 2) { const slice = menu.choices.slice(i, i + 2);