feat: improve model selection UX and fix queue/status behaviors

- Add search/filtering to the `/model` command with multi-word matching
- Finalize partial stream previews (e.g. thinking blocks) on turn abort instead of clearing them
- Dynamically format low-cost `$ value` metrics up to 5 decimal places in status outputs
- Update queue tests to expect text-turn plans for aborted turns with partial text
This commit is contained in:
wassname
2026-04-23 12:42:35 +08:00
parent 64f9d0242f
commit 39da73ce3c
5 changed files with 50 additions and 8 deletions
+6 -3
View File
@@ -1023,6 +1023,7 @@ export default function (pi: ExtensionAPI) {
async function getModelMenuState(
chatId: number,
args: string | undefined,
ctx: ExtensionContext,
): Promise<TelegramModelMenuState> {
const { SettingsManager } = await import("@mariozechner/pi-coding-agent");
@@ -1040,6 +1041,7 @@ export default function (pi: ExtensionAPI) {
availableModels,
configuredScopedModelPatterns: configuredScopedModels,
cliScopedModelPatterns: cliScopedModels ?? undefined,
filterQuery: args,
});
}
@@ -1255,6 +1257,7 @@ export default function (pi: ExtensionAPI) {
async function openModelMenu(
chatId: number,
replyToMessageId: number,
args: string | undefined,
ctx: ExtensionContext,
): Promise<void> {
if (!ctx.isIdle() && !canOfferInFlightTelegramModelSwitch(ctx)) {
@@ -1265,7 +1268,7 @@ export default function (pi: ExtensionAPI) {
);
return;
}
const state = await getModelMenuState(chatId, ctx);
const state = await getModelMenuState(chatId, args, ctx);
if (state.allModels.length === 0) {
await sendTextReply(
chatId,
@@ -1791,8 +1794,8 @@ export default function (pi: ExtensionAPI) {
}
}
const commandName = parseTelegramCommand(rawText)?.name;
const handled = await handleTelegramCommand(commandName, firstMessage, ctx);
const command = parseTelegramCommand(rawText);
const handled = await handleTelegramCommand(command?.name, command?.args, firstMessage, ctx);
if (handled) return;
await enqueueTelegramTurn(messages, ctx);
+17 -4
View File
@@ -124,6 +124,7 @@ export interface BuildTelegramModelMenuStateParams {
availableModels: Model<any>[];
configuredScopedModelPatterns: string[];
cliScopedModelPatterns?: string[];
filterQuery?: string;
}
export type TelegramMenuCallbackAction =
@@ -416,8 +417,17 @@ export function getModelMenuItems(
export function buildTelegramModelMenuState(
params: BuildTelegramModelMenuStateParams,
): TelegramModelMenuState {
let filteredAvailableModels = params.availableModels;
if (params.filterQuery) {
const terms = params.filterQuery.toLowerCase().split(/\s+/).filter(Boolean);
filteredAvailableModels = filteredAvailableModels.filter((m) => {
const target = `${m.provider}/${m.id}`.toLowerCase();
return terms.every((t) => target.includes(t));
});
}
const allModels = sortScopedModels(
params.availableModels.map((model) => ({ model })),
filteredAvailableModels.map((model) => ({ model })),
params.activeModel,
);
const scopedModels =
@@ -425,7 +435,7 @@ export function buildTelegramModelMenuState(
? sortScopedModels(
resolveScopedModelPatterns(
params.configuredScopedModelPatterns,
params.availableModels,
filteredAvailableModels,
),
params.activeModel,
)
@@ -433,17 +443,20 @@ export function buildTelegramModelMenuState(
let note: string | undefined;
if (
params.configuredScopedModelPatterns.length > 0 &&
scopedModels.length === 0
scopedModels.length === 0 &&
!params.filterQuery
) {
note = params.cliScopedModelPatterns
? "No CLI scoped models matched the current auth configuration. Showing all available models."
: "No scoped models matched the current auth configuration. Showing all available models.";
} else if (params.filterQuery && filteredAvailableModels.length === 0) {
note = "No models matched your search query.";
}
return {
chatId: params.chatId,
messageId: 0,
page: 0,
scope: scopedModels.length > 0 ? "scoped" : "all",
scope: (scopedModels.length > 0 && !params.filterQuery) ? "scoped" : "all",
scopedModels,
allModels,
note,
+9
View File
@@ -367,6 +367,15 @@ export function buildTelegramAgentEndPlan(options: {
};
}
if (options.stopReason === "aborted") {
if (options.hasFinalText) {
return {
kind: "text",
shouldClearPreview: false,
shouldDispatchNext,
shouldSendErrorMessage: false,
shouldSendAttachmentNotice: false,
};
}
return {
kind: "aborted",
shouldClearPreview: true,
+8 -1
View File
@@ -65,12 +65,19 @@ function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
}
function formatCost(cost: number): string {
if (cost === 0) return "0.00";
if (cost < 0.001) return cost.toFixed(5);
if (cost < 0.01) return cost.toFixed(4);
return cost.toFixed(3);
}
function buildCostSummary(
stats: TelegramUsageStats,
usesSubscription: boolean,
): string | undefined {
if (!stats.totalCost && !usesSubscription) return undefined;
return `$${stats.totalCost.toFixed(3)}${usesSubscription ? " (sub)" : ""}`;
return `$${formatCost(stats.totalCost)}${usesSubscription ? " (sub)" : ""}`;
}
function buildContextSummary(
+10
View File
@@ -526,6 +526,16 @@ test("Agent end plan classifies turn outcomes correctly", () => {
assert.equal(abortedPlan.kind, "aborted");
assert.equal(abortedPlan.shouldClearPreview, true);
assert.equal(abortedPlan.shouldDispatchNext, false);
const abortedTextPlan = __telegramTestUtils.buildTelegramAgentEndPlan({
hasTurn: true,
stopReason: "aborted",
preserveQueuedTurnsAsHistory: true,
hasFinalText: true,
hasQueuedAttachments: false,
});
assert.equal(abortedTextPlan.kind, "text");
assert.equal(abortedTextPlan.shouldClearPreview, false);
assert.equal(abortedTextPlan.shouldDispatchNext, false);
const errorPlan = __telegramTestUtils.buildTelegramAgentEndPlan({
hasTurn: true,
stopReason: "error",