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( async function getModelMenuState(
chatId: number, chatId: number,
args: string | undefined,
ctx: ExtensionContext, ctx: ExtensionContext,
): Promise<TelegramModelMenuState> { ): Promise<TelegramModelMenuState> {
const { SettingsManager } = await import("@mariozechner/pi-coding-agent"); const { SettingsManager } = await import("@mariozechner/pi-coding-agent");
@@ -1040,6 +1041,7 @@ export default function (pi: ExtensionAPI) {
availableModels, availableModels,
configuredScopedModelPatterns: configuredScopedModels, configuredScopedModelPatterns: configuredScopedModels,
cliScopedModelPatterns: cliScopedModels ?? undefined, cliScopedModelPatterns: cliScopedModels ?? undefined,
filterQuery: args,
}); });
} }
@@ -1255,6 +1257,7 @@ export default function (pi: ExtensionAPI) {
async function openModelMenu( async function openModelMenu(
chatId: number, chatId: number,
replyToMessageId: number, replyToMessageId: number,
args: string | undefined,
ctx: ExtensionContext, ctx: ExtensionContext,
): Promise<void> { ): Promise<void> {
if (!ctx.isIdle() && !canOfferInFlightTelegramModelSwitch(ctx)) { if (!ctx.isIdle() && !canOfferInFlightTelegramModelSwitch(ctx)) {
@@ -1265,7 +1268,7 @@ export default function (pi: ExtensionAPI) {
); );
return; return;
} }
const state = await getModelMenuState(chatId, ctx); const state = await getModelMenuState(chatId, args, ctx);
if (state.allModels.length === 0) { if (state.allModels.length === 0) {
await sendTextReply( await sendTextReply(
chatId, chatId,
@@ -1791,8 +1794,8 @@ export default function (pi: ExtensionAPI) {
} }
} }
const commandName = parseTelegramCommand(rawText)?.name; const command = parseTelegramCommand(rawText);
const handled = await handleTelegramCommand(commandName, firstMessage, ctx); const handled = await handleTelegramCommand(command?.name, command?.args, firstMessage, ctx);
if (handled) return; if (handled) return;
await enqueueTelegramTurn(messages, ctx); await enqueueTelegramTurn(messages, ctx);
+17 -4
View File
@@ -124,6 +124,7 @@ export interface BuildTelegramModelMenuStateParams {
availableModels: Model<any>[]; availableModels: Model<any>[];
configuredScopedModelPatterns: string[]; configuredScopedModelPatterns: string[];
cliScopedModelPatterns?: string[]; cliScopedModelPatterns?: string[];
filterQuery?: string;
} }
export type TelegramMenuCallbackAction = export type TelegramMenuCallbackAction =
@@ -416,8 +417,17 @@ export function getModelMenuItems(
export function buildTelegramModelMenuState( export function buildTelegramModelMenuState(
params: BuildTelegramModelMenuStateParams, params: BuildTelegramModelMenuStateParams,
): TelegramModelMenuState { ): 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( const allModels = sortScopedModels(
params.availableModels.map((model) => ({ model })), filteredAvailableModels.map((model) => ({ model })),
params.activeModel, params.activeModel,
); );
const scopedModels = const scopedModels =
@@ -425,7 +435,7 @@ export function buildTelegramModelMenuState(
? sortScopedModels( ? sortScopedModels(
resolveScopedModelPatterns( resolveScopedModelPatterns(
params.configuredScopedModelPatterns, params.configuredScopedModelPatterns,
params.availableModels, filteredAvailableModels,
), ),
params.activeModel, params.activeModel,
) )
@@ -433,17 +443,20 @@ export function buildTelegramModelMenuState(
let note: string | undefined; let note: string | undefined;
if ( if (
params.configuredScopedModelPatterns.length > 0 && params.configuredScopedModelPatterns.length > 0 &&
scopedModels.length === 0 scopedModels.length === 0 &&
!params.filterQuery
) { ) {
note = params.cliScopedModelPatterns note = params.cliScopedModelPatterns
? "No CLI scoped models matched the current auth configuration. Showing all available models." ? "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."; : "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 { return {
chatId: params.chatId, chatId: params.chatId,
messageId: 0, messageId: 0,
page: 0, page: 0,
scope: scopedModels.length > 0 ? "scoped" : "all", scope: (scopedModels.length > 0 && !params.filterQuery) ? "scoped" : "all",
scopedModels, scopedModels,
allModels, allModels,
note, note,
+9
View File
@@ -367,6 +367,15 @@ export function buildTelegramAgentEndPlan(options: {
}; };
} }
if (options.stopReason === "aborted") { if (options.stopReason === "aborted") {
if (options.hasFinalText) {
return {
kind: "text",
shouldClearPreview: false,
shouldDispatchNext,
shouldSendErrorMessage: false,
shouldSendAttachmentNotice: false,
};
}
return { return {
kind: "aborted", kind: "aborted",
shouldClearPreview: true, shouldClearPreview: true,
+8 -1
View File
@@ -65,12 +65,19 @@ function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
return tokenParts.length > 0 ? tokenParts.join(" ") : 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( function buildCostSummary(
stats: TelegramUsageStats, stats: TelegramUsageStats,
usesSubscription: boolean, usesSubscription: boolean,
): string | undefined { ): string | undefined {
if (!stats.totalCost && !usesSubscription) return undefined; if (!stats.totalCost && !usesSubscription) return undefined;
return `$${stats.totalCost.toFixed(3)}${usesSubscription ? " (sub)" : ""}`; return `$${formatCost(stats.totalCost)}${usesSubscription ? " (sub)" : ""}`;
} }
function buildContextSummary( 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.kind, "aborted");
assert.equal(abortedPlan.shouldClearPreview, true); assert.equal(abortedPlan.shouldClearPreview, true);
assert.equal(abortedPlan.shouldDispatchNext, false); 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({ const errorPlan = __telegramTestUtils.buildTelegramAgentEndPlan({
hasTurn: true, hasTurn: true,
stopReason: "error", stopReason: "error",