fix: finish model list alias + heartbeat session (#1256) (thanks @zknicker)

This commit is contained in:
Peter Steinberger
2026-01-22 01:08:11 +00:00
parent 7725dd6795
commit 39073d5196
9 changed files with 259 additions and 117 deletions

View File

@@ -7,6 +7,7 @@ import {
resolveModelRefFromString,
} from "../../agents/model-selection.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { ReplyPayload } from "../types.js";
import type { CommandHandler } from "./commands-types.js";
@@ -68,10 +69,11 @@ function parseModelsArgs(raw: string): {
};
}
export const handleModelsCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
const body = params.command.commandBodyNormalized.trim();
export async function resolveModelsCommandReply(params: {
cfg: ClawdbotConfig;
commandBodyNormalized: string;
}): Promise<ReplyPayload | null> {
const body = params.commandBodyNormalized.trim();
if (!body.startsWith("/models")) return null;
const argText = body.replace(/^\/models\b/i, "").trim();
@@ -164,7 +166,7 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
"Use: /models <provider>",
"Switch: /model <provider/model>",
];
return { reply: { text: lines.join("\n") }, shouldContinue: false };
return { text: lines.join("\n") };
}
if (!byProvider.has(provider)) {
@@ -176,7 +178,7 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
"",
"Use: /models <provider>",
];
return { reply: { text: lines.join("\n") }, shouldContinue: false };
return { text: lines.join("\n") };
}
const models = [...(byProvider.get(provider) ?? new Set<string>())].sort();
@@ -189,7 +191,7 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
"Browse: /models",
"Switch: /model <provider/model>",
];
return { reply: { text: lines.join("\n") }, shouldContinue: false };
return { text: lines.join("\n") };
}
const effectivePageSize = all ? total : pageSize;
@@ -203,7 +205,7 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
`Try: /models ${provider} ${safePage}`,
`All: /models ${provider} all`,
];
return { reply: { text: lines.join("\n") }, shouldContinue: false };
return { text: lines.join("\n") };
}
const startIndex = (safePage - 1) * effectivePageSize;
@@ -226,5 +228,16 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
}
const payload: ReplyPayload = { text: lines.join("\n") };
return { reply: payload, shouldContinue: false };
return payload;
}
export const handleModelsCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
const reply = await resolveModelsCommandReply({
cfg: params.cfg,
commandBodyNormalized: params.command.commandBodyNormalized,
});
if (!reply) return null;
return { reply, shouldContinue: false };
};

View File

@@ -36,7 +36,7 @@ describe("/model chat UX", () => {
expect(reply?.text).toContain("Switch: /model <provider/model>");
});
it("suggests closest match for typos without switching", () => {
it("auto-applies closest match for typos", () => {
const directives = parseInlineDirectives("/model anthropic/claud-opus-4-5");
const cfg = { commands: { text: true } } as unknown as ClawdbotConfig;
@@ -52,9 +52,11 @@ describe("/model chat UX", () => {
provider: "anthropic",
});
expect(resolved.modelSelection).toBeUndefined();
expect(resolved.errorText).toContain("Did you mean:");
expect(resolved.errorText).toContain("anthropic/claude-opus-4-5");
expect(resolved.errorText).toContain("Try: /model anthropic/claude-opus-4-5");
expect(resolved.modelSelection).toEqual({
provider: "anthropic",
model: "claude-opus-4-5",
isDefault: true,
});
expect(resolved.errorText).toBeUndefined();
});
});

View File

@@ -20,6 +20,7 @@ import {
resolveProviderEndpointLabel,
} from "./directive-handling.model-picker.js";
import type { InlineDirectives } from "./directive-handling.parse.js";
import { resolveModelsCommandReply } from "./commands-models.js";
import { type ModelDirectiveSelection, resolveModelDirectiveSelection } from "./model-selection.js";
function buildModelPickerCatalog(params: {
@@ -185,14 +186,11 @@ export async function maybeHandleModelDirectiveInfo(params: {
});
if (wantsLegacyList) {
return {
text: [
"Model listing moved.",
"",
"Use: /models (providers) or /models <provider> (models)",
"Switch: /model <provider/model>",
].join("\n"),
};
const reply = await resolveModelsCommandReply({
cfg: params.cfg,
commandBodyNormalized: "/models",
});
return reply ?? { text: "No models available." };
}
if (wantsSummary) {
@@ -340,42 +338,7 @@ export function resolveModelSelectionFromDirective(params: {
}
if (resolved.selection) {
const suggestion = `${resolved.selection.provider}/${resolved.selection.model}`;
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 <provider>",
].join("\n"),
};
}
modelSelection = resolved.selection;
}
}