diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e4b141a..3033c6f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.clawd.bot - Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo. - BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204. - Infra: preserve fetch helper methods when wrapping abort signals. (#1387) +- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc. ## 2026.1.22 diff --git a/src/auto-reply/model.test.ts b/src/auto-reply/model.test.ts index a2bbf5f92..4a5aa7714 100644 --- a/src/auto-reply/model.test.ts +++ b/src/auto-reply/model.test.ts @@ -114,10 +114,10 @@ describe("extractModelDirective", () => { }); describe("edge cases", () => { - it("preserves spacing when /model is followed by a path segment", () => { + it("absorbs path-like segments when /model includes extra slashes", () => { const result = extractModelDirective("thats not /model gpt-5/tmp/hello"); expect(result.hasDirective).toBe(true); - expect(result.cleaned).toBe("thats not /hello"); + expect(result.cleaned).toBe("thats not"); }); it("handles alias with special regex characters", () => { diff --git a/src/auto-reply/model.ts b/src/auto-reply/model.ts index 46ea44db1..24dd8ea0c 100644 --- a/src/auto-reply/model.ts +++ b/src/auto-reply/model.ts @@ -14,7 +14,7 @@ export function extractModelDirective( if (!body) return { cleaned: "", hasDirective: false }; const modelMatch = body.match( - /(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)?)?/i, + /(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)*)?/i, ); const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean); diff --git a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts index ffac7deab..45a994432 100644 --- a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts @@ -84,9 +84,9 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Pick: /model <#> or /model "); - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); + expect(text).toContain("Model listing moved."); + expect(text).toContain("Use: /models (providers) or /models (models)"); + expect(text).toContain("Switch: /model "); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -115,9 +115,9 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Pick: /model <#> or /model "); - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); + expect(text).toContain("Current: anthropic/claude-opus-4-5"); + expect(text).toContain("Browse: /models (providers) or /models (models)"); + expect(text).toContain("More: /model status"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -150,10 +150,9 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); - expect(text).toContain("minimax/MiniMax-M2.1"); - expect(text).toContain("xai/grok-4"); + expect(text).toContain("Model listing moved."); + expect(text).toContain("Use: /models (providers) or /models (models)"); + expect(text).toContain("Switch: /model "); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -202,9 +201,9 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("openai/gpt-4.1-mini"); - expect(text).toContain("minimax/MiniMax-M2.1"); + expect(text).toContain("Model listing moved."); + expect(text).toContain("Use: /models (providers) or /models (models)"); + expect(text).toContain("Switch: /model "); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); @@ -231,6 +230,7 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Model listing moved."); expect(text).not.toContain("missing (missing)"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); diff --git a/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts b/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts index 8ed532b0d..c271adef3 100644 --- a/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts @@ -211,10 +211,9 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("anthropic/claude-opus-4-5"); - expect(text).toContain("Pick: /model <#> or /model "); - expect(text).toContain("openai/gpt-4.1-mini"); - expect(text).not.toContain("claude-sonnet-4-1"); + expect(text).toContain("Current: anthropic/claude-opus-4-5"); + expect(text).toContain("Browse: /models (providers) or /models (models)"); + expect(text).toContain("More: /model status"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply/directive-handling.model.ts b/src/auto-reply/reply/directive-handling.model.ts index 0f5782a6f..dd2f98914 100644 --- a/src/auto-reply/reply/directive-handling.model.ts +++ b/src/auto-reply/reply/directive-handling.model.ts @@ -341,16 +341,41 @@ export function resolveModelSelectionFromDirective(params: { if (resolved.selection) { const suggestion = `${resolved.selection.provider}/${resolved.selection.model}`; - return { - errorText: [ - `Unrecognized model: ${raw}`, - "", - `Did you mean: ${suggestion}`, - `Try: /model ${suggestion}`, - "", - "Browse: /models or /models ", - ].join("\n"), - }; + const rawHasSlash = raw.includes("/"); + const shouldAutoSelect = (() => { + if (!rawHasSlash) return true; + const slash = raw.indexOf("/"); + if (slash <= 0) return true; + const rawProvider = normalizeProviderId(raw.slice(0, slash)); + const rawFragment = raw + .slice(slash + 1) + .trim() + .toLowerCase(); + if (!rawFragment) return false; + const resolvedProvider = normalizeProviderId(resolved.selection.provider); + if (rawProvider !== resolvedProvider) return false; + const resolvedModel = resolved.selection.model.toLowerCase(); + return ( + resolvedModel.startsWith(rawFragment) || + resolvedModel.includes(rawFragment) || + rawFragment.startsWith(resolvedModel) + ); + })(); + + if (shouldAutoSelect) { + modelSelection = resolved.selection; + } else { + return { + errorText: [ + `Unrecognized model: ${raw}`, + "", + `Did you mean: ${suggestion}`, + `Try: /model ${suggestion}`, + "", + "Browse: /models or /models ", + ].join("\n"), + }; + } } }