Skip to content

Commit 1968d12

Browse files
committed
wip
1 parent 2c923b3 commit 1968d12

File tree

5 files changed

+207
-92
lines changed

5 files changed

+207
-92
lines changed

chat-client/src/client/mynahUi.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@ import {
5757
} from './utils'
5858
import { ChatHistory, ChatHistoryList } from './features/history'
5959
import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming'
60-
import { paidTierSuccessCard, freeTierLimitReachedCard, upgradeQButton } from './texts/paidTier'
60+
import {
61+
paidTierSuccessCard,
62+
freeTierLimitReachedCard,
63+
freeTierLimitStickyCard,
64+
upgradeQButton,
65+
} from './texts/paidTier'
6166

6267
export interface InboundChatApi {
6368
addChatResponse(params: ChatResult, tabId: string, isPartialResult: boolean): void
@@ -827,34 +832,61 @@ export const createMynahUi = (
827832
* Shows a message if the user reaches free-tier limit.
828833
* Shows a message if the user just upgraded to paid-tier.
829834
*/
830-
const onPaidTierModeChange = (
831-
tabId: string,
832-
mode: 'paidtier' | 'paidtier-success' | 'freetier' | 'freetier-limit'
833-
) => {
834-
if (!['paidtier', 'paidtier-success', 'freetier', 'freetier-limit'].includes(mode)) {
835-
return // invalid mode
835+
const onPaidTierModeChange = (tabId: string, mode: string | undefined) => {
836+
if (
837+
!mode ||
838+
!['freetier', 'freetier-limit', 'freetier-upgrade-pending', 'paidtier', 'paidtier-success'].includes(mode)
839+
) {
840+
return false // invalid mode
836841
}
837842

838843
tabId = tabId !== '' ? tabId : getOrCreateTabId()!
839844

840-
// Detect if the tab is already showing the "Upgrade Q" calls-to-action.
841-
const didShowLimitReached = mynahUi.getTabData(tabId)?.getStore()?.promptInputButtons?.[0] === upgradeQButton
842-
if (mode === 'freetier-limit' && !didShowLimitReached) {
843-
mynahUi.addChatItem(tabId, freeTierLimitReachedCard)
845+
// Detect if the tab is already showing the "Upgrade Q" UI.
846+
const didShowLimitReached =
847+
mynahUi.getTabData(tabId)?.getStore()?.promptInputStickyCard?.messageId ===
848+
freeTierLimitStickyCard.messageId
849+
850+
if (mode === 'freetier-limit') {
851+
mynahUi.updateStore(tabId, {
852+
promptInputStickyCard: freeTierLimitStickyCard,
853+
})
854+
855+
if (!didShowLimitReached) {
856+
// Avoid duplicate "limit reached" cards.
857+
mynahUi.addChatItem(tabId, freeTierLimitReachedCard)
858+
}
859+
} else if (mode === 'freetier-upgrade-pending') {
860+
// Change the sticky banner to show a progress spinner.
861+
const card: typeof freeTierLimitStickyCard = {
862+
...freeTierLimitStickyCard,
863+
icon: 'progress',
864+
}
865+
mynahUi.updateStore(tabId, {
866+
// Show a progress ribbon.
867+
promptInputProgress: {
868+
status: 'default',
869+
text: 'Waiting for subscription status...',
870+
value: -1, // infinite
871+
// valueText: 'Waiting 2...',
872+
},
873+
promptInputStickyCard: card,
874+
})
844875
} else if (mode === 'paidtier-success') {
845876
mynahUi.addChatItem(tabId, paidTierSuccessCard)
846877
}
847878

848879
mynahUi.updateStore(tabId, {
849-
promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
880+
// promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
850881
promptInputDisabledState: mode === 'freetier-limit',
851882
})
883+
884+
return true
852885
}
853886

854887
const updateChat = (params: ChatUpdateParams) => {
855888
// HACK: Special field sent by `agenticChatController.ts:setPaidTierMode()`.
856-
if ((params as any).paidTierMode) {
857-
onPaidTierModeChange(params.tabId, (params as any).paidTierMode as any)
889+
if (onPaidTierModeChange(params.tabId, (params as any).paidTierMode as string)) {
858890
return
859891
}
860892

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
import { ChatItem, ChatItemButton, ChatItemFormItem, ChatItemType, TextBasedFormItem } from '@aws/mynah-ui'
22

3+
export const upgradeQButton: ChatItemButton = {
4+
flash: 'once',
5+
fillState: 'hover',
6+
position: 'outside',
7+
id: 'paidtier-upgrade-q',
8+
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/q.svg
9+
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/rocket.svg
10+
// icon: MynahIcons.Q,
11+
description: 'Upgrade to Amazon Q Pro',
12+
text: 'Upgrade Q',
13+
status: 'info',
14+
}
15+
316
export const freeTierLimitReachedCard: ChatItem = {
417
type: ChatItemType.ANSWER,
518
title: 'FREE TIER LIMIT REACHED',
@@ -14,6 +27,13 @@ export const freeTierLimitReachedCard: ChatItem = {
1427
body: 'You have reached the free tier limit. Upgrade to Amazon Q Pro.\n\n[Learn More...](https://aws.amazon.com/q/pricing/)',
1528
}
1629

30+
/** "Banner" (sticky card) shown above the chat prompt. */
31+
export const freeTierLimitStickyCard: Partial<ChatItem> = {
32+
messageId: 'freetier-limit-banner',
33+
body: 'You have reached the free tier limit. Upgrade to Amazon Q Pro. [Learn More...](https://aws.amazon.com/q/pricing/)',
34+
buttons: [upgradeQButton],
35+
}
36+
1737
export const paidTierSuccessCard: ChatItem = {
1838
type: ChatItemType.ANSWER,
1939
title: 'UPGRADED TO AMAZON Q PRO',
@@ -51,16 +71,3 @@ export const paidTierStep1: ChatItem = {
5171
type: ChatItemType.DIRECTIVE,
5272
body: 'You have upgraded to Amazon Q Pro',
5373
}
54-
55-
export const upgradeQButton: ChatItemButton = {
56-
flash: 'once',
57-
fillState: 'hover',
58-
position: 'outside',
59-
id: 'upgrade-q',
60-
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/q.svg
61-
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/rocket.svg
62-
// icon: MynahIcons.Q,
63-
description: 'Upgrade to Amazon Q Pro',
64-
text: 'Upgrade Q',
65-
status: 'info',
66-
}

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

Lines changed: 113 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export class AgenticChatController implements ChatHandlers {
262262
this.#stoppedToolUses.add(params.messageId)
263263
await this.#renderStoppedShellCommand(params.tabId, params.messageId)
264264
return { success: true }
265-
} else if (params.buttonId === 'upgrade-q') {
265+
} else if (params.buttonId === 'paidtier-upgrade-q') {
266266
const awsAccountId = (params as any).awsAccountId
267267
if (typeof awsAccountId !== 'string') {
268268
this.#log(`invalid awsAccountId: ${awsAccountId}`)
@@ -272,63 +272,14 @@ export class AgenticChatController implements ChatHandlers {
272272
}
273273
}
274274

275-
// Note: intentionally async.
276-
try {
277-
const r = await AmazonQTokenServiceManager.getInstance()
278-
?.getCodewhispererService()
279-
.createSubscriptionToken({
280-
accountId: awsAccountId,
281-
})
282-
283-
if (!r.encodedVerificationUrl) {
284-
this.#log('missing encodedVerificationUrl in server response')
285-
this.#features.lsp.window
286-
.showMessage({
287-
message: 'Subscription request failed. Check the account id.',
288-
type: MessageType.Error,
289-
})
290-
.catch(e => {
291-
this.#log(`showMessage failed: ${(e as Error).message}`)
292-
})
293-
return {
294-
success: false,
295-
failureReason: 'missing encodedVerificationUrl in server response',
296-
}
297-
}
298-
299-
const uri = r.encodedVerificationUrl
300-
301-
try {
302-
URI.parse(uri)
303-
} catch (e) {
304-
this.#log(`invalid encodedVerificationUrl: '${uri}': ${(e as Error).message}`)
305-
return {
306-
success: false,
307-
failureReason: 'invalid encodedVerificationUrl',
308-
}
309-
}
310-
311-
this.#features.lsp.window
312-
.showMessage({
313-
message: 'Upgraded to [Amazon Q Pro](https://aws.amazon.com/q/)',
314-
type: MessageType.Info,
315-
})
316-
.catch(e => {
317-
this.#log(`showMessage failed: ${(e as Error).message}`)
318-
})
319-
320-
this.#features.lsp.window.showDocument({
321-
external: true, // Client is expected to open the URL in a web browser.
322-
uri: uri,
323-
})
324-
} catch (e) {
275+
const errmsg = await this.onPaidTierUpgradeClicked(params.tabId, awsAccountId)
276+
if (errmsg !== '') {
325277
return {
326278
success: false,
327-
failureReason: 'createSubscriptionToken failed',
279+
failureReason: errmsg,
328280
}
329281
}
330282

331-
this.setPaidTierMode(params.tabId, 'paidtier-success')
332283
return { success: true }
333284
} else {
334285
return {
@@ -2112,6 +2063,8 @@ export class AgenticChatController implements ChatHandlers {
21122063
name: ChatTelemetryEventName.EnterFocusConversation,
21132064
data: {},
21142065
})
2066+
2067+
this.setPaidTierMode(params.tabId)
21152068
}
21162069

21172070
onTabRemove(params: TabRemoveParams) {
@@ -2279,32 +2232,30 @@ export class AgenticChatController implements ChatHandlers {
22792232
* - 'paidtier': disable any "free-tier limit" UI.
22802233
*/
22812234
setPaidTierMode(tabId?: string, mode?: PaidTierMode) {
2282-
this.#log(`xxx setPaidTierMode: mode=${mode}`)
2283-
2284-
if (mode === 'freetier-limit') {
2285-
this.#paidTierMode = mode // Sticky until 'paidtier' is sent.
2286-
} else if (mode === 'paidtier') {
2235+
if (this.#paidTierMode === 'freetier-limit' && mode === 'freetier') {
2236+
mode = 'freetier-limit' // Sticky while 'freetier'.
2237+
} else if (mode) {
22872238
this.#paidTierMode = mode
2288-
} else if (this.#paidTierMode === 'freetier-limit' && mode === 'freetier') {
2289-
mode = 'freetier-limit'
2290-
} else if (!mode) {
2239+
} else {
22912240
// Note: intentionally async.
22922241
AmazonQTokenServiceManager.getInstance()
2293-
?.getCodewhispererService()
2242+
.getCodewhispererService()
22942243
.getSubscriptionStatus()
22952244
.then(o => {
2296-
this.#log(`xxx getSubscriptionStatus: ${o.status} ${o.encodedVerificationUrl}`)
2245+
this.#log(`setPaidTierMode: getSubscriptionStatus: ${o.status} ${o.encodedVerificationUrl}`)
22972246
this.setPaidTierMode(tabId, o.status === 'ACTIVE' ? 'paidtier' : 'freetier')
22982247
})
22992248
.catch(err => {
2300-
this.#log(`xxx getSubscriptionStatus failed: ${JSON.stringify(err)}`)
2249+
this.#log(`setPaidTierMode: getSubscriptionStatus failed: ${JSON.stringify(err)}`)
23012250
})
23022251
// const isFreeTierUser = getSsoConnectionType(this.#features.credentialsProvider) === 'builderId'
23032252
// mode = isFreeTierUser ? 'freetier' : 'paidtier'
23042253

23052254
return
23062255
}
23072256

2257+
this.#log(`setPaidTierMode: mode=${mode}`)
2258+
23082259
const o: ChatUpdateParams = {
23092260
tabId: tabId ?? '',
23102261
// data: { messages: [] },
@@ -2314,6 +2265,104 @@ export class AgenticChatController implements ChatHandlers {
23142265
this.#features.chat.sendChatUpdate(o)
23152266
}
23162267

2268+
/**
2269+
* Starts the "Upgrade Q" flow for a free-tier user:
2270+
*
2271+
* 0. `awsAccountId` was provided by the IDE extension.
2272+
* 1. Call `createSubscriptionToken(awsAccountId)`.
2273+
* 2. Set the UI to show "Waiting…" progress indicator.
2274+
* 3. Return result, and...
2275+
* 4. ASYNCHRONOUSLY poll subscription status until success.
2276+
* - Update the UI on success/failure.
2277+
*
2278+
* @param awsAccountId AWS account ID to create subscription for
2279+
* @returns Empty string on success, or error message on failure.
2280+
*/
2281+
async onPaidTierUpgradeClicked(tabId: string, awsAccountId: string): Promise<string> {
2282+
if (typeof awsAccountId !== 'string') {
2283+
this.#log(`invalid awsAccountId: ${awsAccountId}`)
2284+
return 'invalid awsAccountId'
2285+
}
2286+
2287+
try {
2288+
const client = AmazonQTokenServiceManager.getInstance().getCodewhispererService()
2289+
const r = await client.createSubscriptionToken({
2290+
accountId: awsAccountId,
2291+
})
2292+
2293+
if (!r.encodedVerificationUrl) {
2294+
this.#log('missing encodedVerificationUrl in server response')
2295+
this.#features.lsp.window
2296+
.showMessage({
2297+
message: 'Subscription request failed. Check the account id.',
2298+
type: MessageType.Error,
2299+
})
2300+
.catch(e => {
2301+
this.#log(`showMessage failed: ${(e as Error).message}`)
2302+
})
2303+
return 'missing encodedVerificationUrl in server response'
2304+
}
2305+
2306+
const uri = r.encodedVerificationUrl
2307+
2308+
try {
2309+
URI.parse(uri)
2310+
} catch (e) {
2311+
this.#log(`invalid encodedVerificationUrl: '${uri}': ${(e as Error).message}`)
2312+
return 'invalid encodedVerificationUrl'
2313+
}
2314+
2315+
this.#log(`createSubscriptionToken status: ${r.status} encodedVerificationUrl: '${uri}'`)
2316+
// Set UI to "progress" mode.
2317+
this.setPaidTierMode(tabId, 'freetier-upgrade-pending')
2318+
2319+
// Navigate user to the browser, where they will complete "Upgrade Q" flow.
2320+
this.#features.lsp.window.showDocument({
2321+
external: true, // Client is expected to open the URL in a web browser.
2322+
uri: uri,
2323+
})
2324+
2325+
// Now asynchronously wait for the user to complete the "Upgrade Q" flow.
2326+
client
2327+
.waitUntilSubscriptionActive()
2328+
.then(r => {
2329+
if (r !== true) {
2330+
this.setPaidTierMode(tabId, 'freetier')
2331+
2332+
this.#features.lsp.window
2333+
.showMessage({
2334+
message: 'Timeout or cancellation while waiting for Amazon Q subscription',
2335+
type: MessageType.Error,
2336+
})
2337+
.catch(e => {
2338+
this.#log(`showMessage failed: ${(e as Error).message}`)
2339+
})
2340+
2341+
return
2342+
}
2343+
2344+
this.setPaidTierMode(tabId, 'paidtier-success')
2345+
2346+
this.#features.lsp.window
2347+
.showMessage({
2348+
message: 'Upgraded to [Amazon Q Pro](https://aws.amazon.com/q/)',
2349+
type: MessageType.Info,
2350+
})
2351+
.catch(e => {
2352+
this.#log(`showMessage failed: ${(e as Error).message}`)
2353+
})
2354+
})
2355+
.catch(e => {
2356+
this.#log(`waitUntilSubscriptionActive failed: ${(e as Error).message}`)
2357+
})
2358+
2359+
return ''
2360+
} catch (e) {
2361+
this.#log(`createSubscriptionToken failed: ${(e as Error).message}`)
2362+
return 'Failed to create subscription token'
2363+
}
2364+
}
2365+
23172366
async #processGenerateAssistantResponseResponseWithTimeout(
23182367
response: GenerateAssistantResponseCommandOutput,
23192368
metric: Metric<AddMessageEvent>,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export type PaidTierMode = 'paidtier' | 'paidtier-success' | 'freetier' | 'freetier-limit'
1+
export type PaidTierMode = 'freetier' | 'freetier-limit' | 'freetier-upgrade-pending' | 'paidtier' | 'paidtier-success'

0 commit comments

Comments
 (0)