From a618b8c50480ed392e824c91185a903e9695826e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 13:40:42 +0000 Subject: [PATCH 1/2] Add CS-10438 plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matic Jurglič --- ...38-assistant-credit-message-timing-plan.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/cs-10438-assistant-credit-message-timing-plan.md diff --git a/docs/cs-10438-assistant-credit-message-timing-plan.md b/docs/cs-10438-assistant-credit-message-timing-plan.md new file mode 100644 index 0000000000..7c654d08a4 --- /dev/null +++ b/docs/cs-10438-assistant-credit-message-timing-plan.md @@ -0,0 +1,28 @@ +## Goal +- Prevent the AI assistant from showing “Credits added!” unless we have + evidence the balance increased after an out-of-credits error. + +## Assumptions +- The out-of-credits error text can appear even when the cached balance + is already above the minimum, so we should avoid claiming credits were + added in that case. +- It is still desirable to show “Credits added!” after the balance + transitions from below-minimum to above-minimum while the error is + displayed. + +## Plan +1. Update `AiAssistantMessage` to track whether the balance was below the + minimum the first time an out-of-credits error is shown. +2. Only render the “Credits added!” label when the error was first shown + while below the minimum and the balance is now above it. +3. Add an integration test that simulates an out-of-credits error while + the billing service already reports sufficient credits, asserting that + “Credits added!” does not render (but Retry does). + +## Target Files +- `packages/host/app/components/ai-assistant/message/index.gts` +- `packages/host/tests/integration/components/ai-assistant-panel/general-test.gts` + +## Testing Notes +- Run `pnpm lint` in `packages/host`. +- If feasible, run a focused Ember test for the new scenario. From 2a88ff0fd7a79837dd4025f731b2fc58ffc0f3e6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Mar 2026 13:55:48 +0000 Subject: [PATCH 2/2] Fix credits added message timing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matic Jurglič --- .../components/ai-assistant/message/index.gts | 57 +++++++++++++++++-- .../ai-assistant-panel/general-test.gts | 33 +++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/host/app/components/ai-assistant/message/index.gts b/packages/host/app/components/ai-assistant/message/index.gts index c275360a6c..8fc37fc444 100644 --- a/packages/host/app/components/ai-assistant/message/index.gts +++ b/packages/host/app/components/ai-assistant/message/index.gts @@ -5,6 +5,7 @@ import { action } from '@ember/object'; import { service } from '@ember/service'; import type { SafeString } from '@ember/template'; import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import Modifier from 'ember-modifier'; import throttle from 'lodash/throttle'; @@ -233,6 +234,28 @@ class ScrollPosition extends Modifier { } } +interface OutOfCreditsSnapshotSignature { + Args: { + Named: { + isOutOfCredits: boolean; + recordSnapshot: (isOutOfCredits: boolean) => void; + }; + }; +} + +class OutOfCreditsSnapshot extends Modifier { + modify( + _element: HTMLElement, + _positional: [], + { + isOutOfCredits, + recordSnapshot, + }: OutOfCreditsSnapshotSignature['Args']['Named'], + ) { + recordSnapshot(isOutOfCredits); + } +} + function isThinkingMessage(s: string | null | undefined) { if (!s) { return false; @@ -256,6 +279,8 @@ export default class AiAssistantMessage extends Component { @service declare private operatorModeStateService: OperatorModeStateService; @service declare private billingService: BillingService; + @tracked private wasOutOfCreditsAtError: boolean | undefined; + private get isReasoningExpandedByDefault() { let result = this.args.isStreaming && @@ -354,7 +379,14 @@ export default class AiAssistantMessage extends Component { {{#if this.errorMessages.length}} {{#if this.isOutOfCreditsErrorMessage}} - + {{#if this.isOutOfCredits}} { /> {{else if @retryAction}}
-
- Credits added! -
+ {{#if this.shouldShowCreditsAdded}} +
+ Credits added! +
+ {{/if}}
{{/if}} @@ -506,6 +540,21 @@ export default class AiAssistantMessage extends Component { private get isOutOfCredits() { return !this.hasMinimumCreditsToContinue; } + + @action + private recordOutOfCreditsSnapshot(isOutOfCredits: boolean) { + if (this.wasOutOfCreditsAtError === undefined) { + this.wasOutOfCreditsAtError = isOutOfCredits; + } + } + + private get shouldShowCreditsAdded() { + return ( + this.isOutOfCreditsErrorMessage && + !this.isOutOfCredits && + this.wasOutOfCreditsAtError === true + ); + } } interface AiAssistantConversationSignature { diff --git a/packages/host/tests/integration/components/ai-assistant-panel/general-test.gts b/packages/host/tests/integration/components/ai-assistant-panel/general-test.gts index c06037c1fe..e7c4fe1525 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel/general-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel/general-test.gts @@ -857,6 +857,39 @@ module('Integration | ai-assistant-panel | general', function (hooks) { assert.dom('[data-test-credits-added]').exists(); }); + test('it does not claim credits added when balance is already sufficient', async function (assert) { + let roomId = await renderAiAssistantPanel(); + + let billingService = getService('billing-service'); + let attributes = { + creditsAvailableInPlanAllowance: 20, + extraCreditsAvailableInBalance: 0, + }; + + billingService.fetchSubscriptionData = async () => { + return new Response(JSON.stringify({ data: { attributes } })); + }; + + await billingService.loadSubscriptionData(); + await settled(); + + simulateRemoteMessage(roomId, '@aibot:localhost', { + body: 'You need a minimum of 10 credits to continue using the AI bot. Please upgrade to a larger plan, or top up your account.', + msgtype: 'm.text', + format: 'org.matrix.custom.html', + isStreamingFinished: true, + errorMessage: + 'You need a minimum of 10 credits to continue using the AI bot. Please upgrade to a larger plan, or top up your account.', + }); + + await waitFor('[data-test-message-idx="0"]'); + assert.dom('[data-test-alert-action-button="Retry"]').exists(); + assert.dom('[data-test-credits-added]').doesNotExist(); + assert + .dom('[data-test-alert-action-button="Buy More Credits"]') + .doesNotExist(); + }); + test('it can retry a message when receiving an error from the AI bot', async function (assert) { let roomId = await renderAiAssistantPanel();