From 39da73ce3c0da332daf234a64195174f4ff1a596 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:42:35 +0800 Subject: [PATCH] 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 --- index.ts | 9 ++++++--- lib/menu.ts | 21 +++++++++++++++++---- lib/queue.ts | 9 +++++++++ lib/status.ts | 9 ++++++++- tests/queue.test.ts | 10 ++++++++++ 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index 210f20c..cd4975d 100644 --- a/index.ts +++ b/index.ts @@ -1023,6 +1023,7 @@ export default function (pi: ExtensionAPI) { async function getModelMenuState( chatId: number, + args: string | undefined, ctx: ExtensionContext, ): Promise { 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 { 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); diff --git a/lib/menu.ts b/lib/menu.ts index 737ecce..c716191 100644 --- a/lib/menu.ts +++ b/lib/menu.ts @@ -124,6 +124,7 @@ export interface BuildTelegramModelMenuStateParams { availableModels: Model[]; 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, diff --git a/lib/queue.ts b/lib/queue.ts index 1a65c57..ffd54a0 100644 --- a/lib/queue.ts +++ b/lib/queue.ts @@ -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, diff --git a/lib/status.ts b/lib/status.ts index 3b9badc..587cabc 100644 --- a/lib/status.ts +++ b/lib/status.ts @@ -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( diff --git a/tests/queue.test.ts b/tests/queue.test.ts index 48cb8be..bb5f53c 100644 --- a/tests/queue.test.ts +++ b/tests/queue.test.ts @@ -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",