Skip to content

Commit 189e1f5

Browse files
committed
wip
1 parent f470d4c commit 189e1f5

File tree

4 files changed

+107
-40
lines changed

4 files changed

+107
-40
lines changed

chat-client/src/client/mynahUi.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ import { ChatHistory, ChatHistoryList } from './features/history'
5959
import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming'
6060
import { getModelSelectionChatItem } from './texts/modelSelection'
6161
import {
62-
paidTierInfoCard,
6362
freeTierLimitSticky,
6463
upgradeSuccessSticky,
6564
upgradePendingSticky,
6665
plansAndPricingTitle,
66+
freeTierLimitDirective,
6767
} from './texts/paidTier'
6868

6969
export interface InboundChatApi {
@@ -868,23 +868,45 @@ export const createMynahUi = (
868868
}
869869

870870
tabId = !!tabId ? tabId : getOrCreateTabId()!
871+
const store = mynahUi.getTabData(tabId).getStore() || {}
871872

872873
// Detect if the tab is already showing the "Upgrade Q" UI.
873-
const isFreeTierLimitUi =
874-
mynahUi.getTabData(tabId)?.getStore()?.promptInputStickyCard?.messageId === freeTierLimitSticky.messageId
875-
const isUpgradePendingUi =
876-
mynahUi.getTabData(tabId)?.getStore()?.promptInputStickyCard?.messageId === upgradePendingSticky.messageId
877-
const isPlansAndPricingTab = plansAndPricingTitle === mynahUi.getTabData(tabId).getStore()?.tabTitle
874+
const isFreeTierLimitUi = store.promptInputStickyCard?.messageId === freeTierLimitSticky.messageId
875+
const isUpgradePendingUi = store.promptInputStickyCard?.messageId === upgradePendingSticky.messageId
876+
const isPlansAndPricingTab = plansAndPricingTitle === store.tabTitle
878877

879878
if (mode === 'freetier-limit') {
880879
mynahUi.updateStore(tabId, {
881880
promptInputStickyCard: freeTierLimitSticky,
882881
})
883882

884883
if (!isFreeTierLimitUi) {
885-
// Avoid duplicate "limit reached" cards.
886-
// REMOVED: don't want the "card", just use the "banner" only.
887-
// mynahUi.addChatItem(tabId, freeTierLimitCard)
884+
// TODO: how to set a warning icon on the user's failed prompt?
885+
//
886+
// const chatItems = store.chatItems ?? []
887+
// const lastPrompt = chatItems.filter(ci => ci.type === ChatItemType.PROMPT).at(-1)
888+
// for (const c of chatItems) {
889+
// c.body = 'xxx / ' + c.type
890+
// c.icon = 'warning'
891+
// c.iconStatus = 'warning'
892+
// c.status = 'warning'
893+
// }
894+
//
895+
// if (lastPrompt && lastPrompt.messageId) {
896+
// lastPrompt.icon = 'warning'
897+
// lastPrompt.iconStatus = 'warning'
898+
// lastPrompt.status = 'warning'
899+
//
900+
// // Decorate the failed prompt with a warning icon.
901+
// // mynahUi.updateChatAnswerWithMessageId(tabId, lastPrompt.messageId, lastPrompt)
902+
// }
903+
//
904+
// mynahUi.updateStore(tabId, {
905+
// chatItems: chatItems,
906+
// })
907+
} else {
908+
// Show directive only on 2nd chat attempt, not the initial attempt.
909+
mynahUi.addChatItem(tabId, freeTierLimitDirective)
888910
}
889911
} else if (mode === 'upgrade-pending') {
890912
// Change the sticky banner to show a progress spinner.

chat-client/src/client/texts/paidTier.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ export const freeTierLimitCard: ChatItem = {
5555
body: `You have reached the free tier limit. Upgrade to Amazon Q Pro.\n\n[Learn More...](${paidTierLearnMoreUrl})`,
5656
}
5757

58+
export const freeTierLimitDirective: ChatItem = {
59+
type: ChatItemType.DIRECTIVE,
60+
// title: '...',
61+
// header: { },
62+
messageId: 'freetier-limit-directive',
63+
fullWidth: true,
64+
contentHorizontalAlignment: 'center',
65+
canBeDismissed: false,
66+
body: 'Unable to send. Monthly invocation limit met for this month.',
67+
}
68+
5869
/** "Banner" (sticky card) shown above the chat prompt. */
5970
export const freeTierLimitSticky: Partial<ChatItem> = {
6071
messageId: 'freetier-limit-banner',

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2502,10 +2502,10 @@ export class AgenticChatController implements ChatHandlers {
25022502
// Note: intentionally async.
25032503
AmazonQTokenServiceManager.getInstance()
25042504
.getCodewhispererService()
2505-
.getSubscriptionStatus()
2505+
.getSubscriptionStatus(true)
25062506
.then(o => {
25072507
this.#log(`setPaidTierMode: getSubscriptionStatus: ${o.status} ${o.encodedVerificationUrl}`)
2508-
this.setPaidTierMode(tabId, o.status === 'ACTIVE' ? 'paidtier' : 'freetier')
2508+
this.setPaidTierMode(tabId, o.status !== 'none' ? 'paidtier' : 'freetier')
25092509
})
25102510
.catch(err => {
25112511
this.#log(`setPaidTierMode: getSubscriptionStatus failed: ${JSON.stringify(err)}`)
@@ -2560,14 +2560,10 @@ export class AgenticChatController implements ChatHandlers {
25602560
.getSubscriptionStatus()
25612561
.then(o => {
25622562
this.#log(`onManageSubscription: getSubscriptionStatus: ${o.status} ${o.encodedVerificationUrl}`)
2563-
const uri =
2564-
o.status === 'ACTIVE'
2565-
? // Paid-tier user: navigate them to the "Manage Subscriptions" AWS console page.
2566-
paidTierLearnMoreUrl
2567-
: // Free-tier user: navigate them to "Upgrade Q" flow in AWS console.
2568-
o.encodedVerificationUrl
2569-
if (o.status === 'ACTIVE') {
2570-
// Navigate user to the browser URL..
2563+
2564+
if (o.status !== 'none') {
2565+
// Paid-tier user: navigate them to the "Manage Subscriptions" AWS console page.
2566+
const uri = paidTierLearnMoreUrl
25712567
this.#features.lsp.window
25722568
.showDocument({
25732569
external: true, // Client is expected to open the URL in a web browser.
@@ -2577,6 +2573,7 @@ export class AgenticChatController implements ChatHandlers {
25772573
this.#log(`onManageSubscription: showDocument failed: ${fmtError(e)}`)
25782574
})
25792575
} else {
2576+
// Free-tier user: navigate them to "Upgrade Q" flow in AWS console.
25802577
const uri = o.encodedVerificationUrl
25812578

25822579
if (!uri) {

server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface GenerateSuggestionsResponse {
4848

4949
import CodeWhispererSigv4Client = require('../client/sigv4/codewhisperersigv4client')
5050
import CodeWhispererTokenClient = require('../client/token/codewhispererbearertokenclient')
51+
import { getErrorId } from './utils'
5152

5253
// Right now the only difference between the token client and the IAM client for codewhsiperer is the difference in function name
5354
// This abstract class can grow in the future to account for any additional changes across the clients
@@ -156,8 +157,8 @@ export class CodeWhispererServiceIAM extends CodeWhispererServiceBase {
156157
*/
157158
export class CodeWhispererServiceToken extends CodeWhispererServiceBase {
158159
client: CodeWhispererTokenClient
159-
/** Debounce getSubscriptionStatus by storing the current, pending promise (if any). */
160-
#getSubscriptionStatusPromise: ReturnType<typeof this.createSubscriptionToken> | undefined
160+
/** Debounce createSubscriptionToken by storing the current, pending promise (if any). */
161+
#createSubscriptionTokenPromise: Promise<CodeWhispererTokenClient.CreateSubscriptionTokenResponse> | undefined
161162

162163
constructor(
163164
private credentialsProvider: CredentialsProvider,
@@ -368,34 +369,70 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase {
368369
}
369370

370371
/**
372+
* (debounced by default)
373+
*
371374
* cool api you have there 🥹
372375
*/
373376
async createSubscriptionToken(request: CodeWhispererTokenClient.CreateSubscriptionTokenRequest) {
374-
return this.client.createSubscriptionToken(this.withProfileArn(request)).promise()
375-
}
376-
377-
/**
378-
* Gets the Subscription status of the given user.
379-
*/
380-
async getSubscriptionStatus(): ReturnType<typeof this.createSubscriptionToken> {
381377
// Debounce.
382-
if (this.#getSubscriptionStatusPromise) {
383-
// this.logging.debug('getSubscriptionStatus: debounced')
384-
return this.#getSubscriptionStatusPromise
378+
if (this.#createSubscriptionTokenPromise) {
379+
// this.logging.debug('createSubscriptionTokenPromise: debounced')
380+
return this.#createSubscriptionTokenPromise
385381
}
386382

387-
this.#getSubscriptionStatusPromise = (async () => {
383+
this.#createSubscriptionTokenPromise = (async () => {
388384
try {
389-
const resp = await this.createSubscriptionToken({
390-
// clientToken: this.credentialsProvider.getCredentials('bearer').token,
391-
})
392-
return resp
385+
return this.client.createSubscriptionToken(this.withProfileArn(request)).promise()
393386
} finally {
394-
this.#getSubscriptionStatusPromise = undefined
387+
this.#createSubscriptionTokenPromise = undefined
395388
}
396389
})()
397390

398-
return this.#getSubscriptionStatusPromise
391+
return this.#createSubscriptionTokenPromise
392+
}
393+
394+
/**
395+
* Gets the Subscription status of the given user.
396+
*
397+
* @param statusOnly use this if you don't need the encodedVerificationUrl, else a ConflictException is treated as "ACTIVE"
398+
*/
399+
async getSubscriptionStatus(
400+
statusOnly?: boolean
401+
): Promise<{ status: 'active' | 'active-expiring' | 'none'; encodedVerificationUrl?: string }> {
402+
// NOTE: The subscription API behaves in a non-intuitive way.
403+
// https://github.com/aws/amazon-q-developer-cli-autocomplete/blob/86edd86a338b549b5192de67c9fdef240e6014b7/crates/chat-cli/src/cli/chat/mod.rs#L4079-L4102
404+
//
405+
// If statusOnly=true, the service only returns "ACTIVE" and "INACTIVE".
406+
// If statusOnly=false, the following spec applies:
407+
//
408+
// 1. "ACTIVE" => 'active-expiring':
409+
// - Active but cancelled. User *has* a subscription, but set to *not auto-renew* (i.e., cancelled).
410+
// 2. "INACTIVE" => 'none':
411+
// - User has no subscription at all (no Pro access).
412+
// 3. ConflictException => 'active':
413+
// - User has an active subscription *with auto-renewal enabled*.
414+
//
415+
// Also, it is currently not possible to subscribe or re-subscribe via console, only IDE/CLI.
416+
try {
417+
const r = await this.createSubscriptionToken({
418+
statusOnly: !!statusOnly,
419+
// clientToken: this.credentialsProvider.getCredentials('bearer').token,
420+
})
421+
const status = r.status === 'ACTIVE' ? 'active-expiring' : 'none'
422+
423+
return {
424+
status: status,
425+
encodedVerificationUrl: r.encodedVerificationUrl,
426+
}
427+
} catch (e) {
428+
if (getErrorId(e as Error) === 'ConflictException') {
429+
return {
430+
status: 'active',
431+
}
432+
}
433+
434+
throw e
435+
}
399436
}
400437

401438
/**
@@ -409,9 +446,9 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase {
409446
if (cancelToken?.isCancellationRequested) {
410447
return false
411448
}
412-
const s = await this.getSubscriptionStatus()
449+
const s = await this.getSubscriptionStatus(true)
413450
this.logging.info(`waitUntilSubscriptionActive: ${s.status}`)
414-
if (s.status === 'ACTIVE') {
451+
if (s.status !== 'none') {
415452
return true
416453
}
417454
},

0 commit comments

Comments
 (0)