diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index a6014e8f9..1f8d44242 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -124,6 +124,25 @@ describe("directive parsing", () => { expect(res.thinkLevel).toBe("high"); }); + it("does not match /think followed by extra letters", () => { + // e.g. someone typing "/think" + extra letter "hink" + const res = extractThinkDirective("/thinkstuff"); + expect(res.hasDirective).toBe(false); + }); + + it("matches /think with no argument", () => { + const res = extractThinkDirective("/think"); + expect(res.hasDirective).toBe(true); + expect(res.thinkLevel).toBeUndefined(); + expect(res.rawLevel).toBeUndefined(); + }); + + it("matches /t with no argument", () => { + const res = extractThinkDirective("/t"); + expect(res.hasDirective).toBe(true); + expect(res.thinkLevel).toBeUndefined(); + }); + it("matches queue directive", () => { const res = extractQueueDirective("please /queue interrupt now"); expect(res.hasDirective).toBe(true); @@ -355,6 +374,51 @@ describe("directive parsing", () => { }); }); + it("shows current think level when /think has no argument", async () => { + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockReset(); + + const res = await getReplyFromConfig( + { Body: "/think", From: "+1222", To: "+1222" }, + {}, + { + agent: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + thinkingDefault: "high", + }, + session: { store: path.join(home, "sessions.json") }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Current thinking level: high"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + + it("shows off when /think has no argument and no default set", async () => { + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockReset(); + + const res = await getReplyFromConfig( + { Body: "/think", From: "+1222", To: "+1222" }, + {}, + { + agent: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + }, + session: { store: path.join(home, "sessions.json") }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Current thinking level: off"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("rejects invalid elevated level", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockReset(); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index e7f95bb0d..a63c33cbd 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -469,6 +469,9 @@ export async function getReplyFromConfig( isGroup, }) ) { + const currentThinkLevel = + (sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? + (agentCfg?.thinkingDefault as ThinkLevel | undefined); const directiveReply = await handleDirectiveOnly({ cfg, directives, @@ -488,6 +491,7 @@ export async function getReplyFromConfig( model, initialModelLabel, formatModelSwitchEvent, + currentThinkLevel, }); typing.cleanup(); return directiveReply; diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index 5c368721c..9b0276786 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -309,6 +309,7 @@ export async function handleDirectiveOnly(params: { model: string; initialModelLabel: string; formatModelSwitchEvent: (label: string, alias?: string) => string; + currentThinkLevel?: ThinkLevel; }): Promise { const { directives, @@ -326,6 +327,7 @@ export async function handleDirectiveOnly(params: { resetModelOverride, initialModelLabel, formatModelSwitchEvent, + currentThinkLevel, } = params; if (directives.hasModelDirective) { @@ -379,8 +381,13 @@ export async function handleDirectiveOnly(params: { } if (directives.hasThinkDirective && !directives.thinkLevel) { + // If no argument was provided, show the current level + if (!directives.rawThinkLevel) { + const level = currentThinkLevel ?? "off"; + return { text: `Current thinking level: ${level}.` }; + } return { - text: `Unrecognized thinking level "${directives.rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`, + text: `Unrecognized thinking level "${directives.rawThinkLevel}". Valid levels: off, minimal, low, medium, high.`, }; } if (directives.hasVerboseDirective && !directives.verboseLevel) { diff --git a/src/auto-reply/reply/directives.ts b/src/auto-reply/reply/directives.ts index f1eacaccb..613ded005 100644 --- a/src/auto-reply/reply/directives.ts +++ b/src/auto-reply/reply/directives.ts @@ -16,9 +16,9 @@ export function extractThinkDirective(body?: string): { hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - // Match the longest keyword first to avoid partial captures (e.g. "/think:high") + // Match with optional argument - require word boundary via lookahead after keyword const match = body.match( - /(?:^|\s)\/(?:thinking|think|t)\s*:?\s*([a-zA-Z-]+)\b/i, + /(?:^|\s)\/(?:thinking|think|t)(?=$|\s|:)(?:\s*:?\s*([a-zA-Z-]+)\b)?/i, ); const thinkLevel = normalizeThinkLevel(match?.[1]); const cleaned = match