From a0c4aa5d7c81e173552536fdee3b900146c6cf80 Mon Sep 17 00:00:00 2001 From: Aarush Arora Date: Wed, 16 Sep 2026 12:28:14 -0700 Subject: [PATCH 1/3] feat(chat-client): persist deprecation notice dismissal --- chat-client/src/client/chat.ts | 4 +- chat-client/src/client/mynahUi.test.ts | 47 ++++++++++++++-- chat-client/src/client/mynahUi.ts | 32 +++++++++-- .../src/client/tabs/tabFactory.test.ts | 54 ++++++++++++++++++- chat-client/src/client/tabs/tabFactory.ts | 9 ++-- .../src/client/texts/deprecation.test.ts | 22 ++++++++ chat-client/src/client/texts/deprecation.ts | 17 ++++++ 7 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 chat-client/src/client/texts/deprecation.test.ts create mode 100644 chat-client/src/client/texts/deprecation.ts diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index 8d52b0b345..de68ce62d4 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -130,6 +130,7 @@ const getDefaultTabConfig = (agenticMode?: boolean) => { type ChatClientConfig = Pick & { disclaimerAcknowledged?: boolean pairProgrammingAcknowledged?: boolean + deprecationNoticeAcknowledged?: boolean agenticMode?: boolean modelSelectionEnabled?: boolean stringOverrides?: Partial @@ -356,7 +357,7 @@ export const createChat = ( // that tab does not have banner message, which arrives in ChatOptions above. const store = mynahUi.getTabData(tabFactory.initialTabId)?.getStore() || {} const chatItems = store.chatItems || [] - const updatedInitialItems = tabFactory.getChatItems(false, false, chatItems as ChatMessage[]) + const updatedInitialItems = tabFactory.getChatItems(false, false, false, chatItems as ChatMessage[]) // First clear the tab, so that messages are not appended https://github.com/aws/mynah-ui/blob/38608dff905b3790d85c73e2911ec7071c8a8cdf/docs/USAGE.md#using-updatestore-function mynahUi.updateStore(tabFactory.initialTabId, { @@ -572,6 +573,7 @@ export const createChat = ( tabFactory, config?.disclaimerAcknowledged ?? false, config?.pairProgrammingAcknowledged ?? false, + config?.deprecationNoticeAcknowledged ?? false, chatClientAdapter, featureConfig, !!config?.agenticMode, diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts index a69cf883c7..54ba171d0e 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -15,7 +15,8 @@ import { ChatItemType, MynahUI, NotificationType } from '@aws/mynah-ui' import { ChatClientAdapter } from '../contracts/chatClientAdapter' import { ChatMessage, ContextCommand, ListAvailableModelsResult } from '@aws/language-server-runtimes-types' import { ChatHistory } from './features/history' -import { pairProgrammingModeOn, pairProgrammingModeOff } from './texts/pairProgramming' +import { pairProgrammingModeOn, pairProgrammingModeOff, programmerModeCard } from './texts/pairProgramming' +import { deprecationCard } from './texts/deprecation' import { strictEqual } from 'assert' describe('MynahUI', () => { @@ -91,7 +92,7 @@ describe('MynahUI', () => { createTabStub.returns({}) getChatItemsStub = sinon.stub(tabFactory, 'getChatItems') getChatItemsStub.returns([]) - const mynahUiResult = createMynahUi(messager, tabFactory, true, true, undefined, undefined, true) + const mynahUiResult = createMynahUi(messager, tabFactory, true, false, false, undefined, undefined, true) mynahUi = mynahUiResult[0] inboundChatApi = mynahUiResult[1] getSelectedTabIdStub = sinon.stub(mynahUi, 'getSelectedTabId') @@ -170,7 +171,7 @@ describe('MynahUI', () => { inboundChatApi.openTab(requestId, {}) sinon.assert.calledOnceWithExactly(createTabStub, false) - sinon.assert.calledOnceWithExactly(getChatItemsStub, true, false, undefined) + sinon.assert.calledOnceWithExactly(getChatItemsStub, true, true, true, undefined) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -201,7 +202,7 @@ describe('MynahUI', () => { }) sinon.assert.calledOnceWithExactly(createTabStub, false) - sinon.assert.calledOnceWithExactly(getChatItemsStub, false, false, mockMessages) + sinon.assert.calledOnceWithExactly(getChatItemsStub, false, true, true, mockMessages) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -760,6 +761,7 @@ describe('MynahUI', () => { tabFactory, true, true, + true, undefined, undefined, true, @@ -776,6 +778,42 @@ describe('MynahUI', () => { strictEqual(configTexts.clickFileToViewDiff, uiComponentsTexts.clickFileToViewDiff) }) }) + + describe('onMessageDismiss', () => { + it('acknowledges the deprecation card and removes it from future new chats', () => { + const updateTabDefaultsSpy = sinon.spy(mynahUi, 'updateTabDefaults') + + ;(mynahUi as any).props.onMessageDismiss('tab-1', deprecationCard.messageId) + + sinon.assert.calledWithExactly( + outboundChatApi.chatPromptOptionAcknowledged as sinon.SinonStub, + deprecationCard.messageId + ) + sinon.assert.calledWithExactly(getChatItemsStub, true, true, false) + sinon.assert.calledWithExactly(updateTabDefaultsSpy, { + store: { + chatItems: [], + }, + }) + }) + + it('acknowledges the agentic feature card without removing the deprecation card from future new chats', () => { + const updateTabDefaultsSpy = sinon.spy(mynahUi, 'updateTabDefaults') + + ;(mynahUi as any).props.onMessageDismiss('tab-1', programmerModeCard.messageId) + + sinon.assert.calledWithExactly( + outboundChatApi.chatPromptOptionAcknowledged as sinon.SinonStub, + programmerModeCard.messageId + ) + sinon.assert.calledWithExactly(getChatItemsStub, true, false, true) + sinon.assert.calledWithExactly(updateTabDefaultsSpy, { + store: { + chatItems: [], + }, + }) + }) + }) }) describe('withAdapter', () => { @@ -810,6 +848,7 @@ describe('withAdapter', () => { tabFactory, true, true, + true, chatClientAdapter, undefined, true diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 5f24c71e45..6a56fc3fa5 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -70,6 +70,7 @@ import { } from './utils' import { ChatHistory, ChatHistoryList } from './features/history' import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming' +import { deprecationCard } from './texts/deprecation' import { ContextRule, RulesList } from './features/rules' import { getModelSelectionChatItem, modelUnavailableBanner, modelThrottledBanner } from './texts/modelSelection' import { getWelcomeTabHeader } from './texts/welcome' @@ -324,6 +325,7 @@ export const createMynahUi = ( tabFactory: TabFactory, disclaimerAcknowledged: boolean, pairProgrammingCardAcknowledged: boolean, + deprecationNoticeAcknowledged: boolean, customChatClientAdapter?: ChatClientAdapter, featureConfig?: Map, agenticMode?: boolean, @@ -332,6 +334,7 @@ export const createMynahUi = ( ): [MynahUI, InboundChatApi] => { let disclaimerCardActive = !disclaimerAcknowledged let programmingModeCardActive = !pairProgrammingCardAcknowledged + let deprecationCardActive = !deprecationNoticeAcknowledged let contextCommandGroups: ContextCommandGroups | undefined let lastFilterTabId: string | undefined @@ -434,7 +437,12 @@ export const createMynahUi = ( // We check if tabMetadata.openTabKey exists - if it does and is set to true, we skip showing welcome messages // since this indicates we're loading a previous chat session rather than starting a new one. if (!tabStore?.tabMetadata || !tabStore.tabMetadata.openTabKey) { - defaultTabConfig.chatItems = tabFactory.getChatItems(true, programmingModeCardActive, []) + defaultTabConfig.chatItems = tabFactory.getChatItems( + true, + programmingModeCardActive, + deprecationCardActive, + [] + ) // Roll a fresh "Did you know?" tip for every new tab. The // mynah-ui defaults.store is built once at startup, so without // this override every new tab would inherit the same cached tip. @@ -712,14 +720,23 @@ export const createMynahUi = ( messager.onPromptInputButtonClick(payload) }, onMessageDismiss: (tabId, messageId) => { + let promptOptionAcknowledged = false + if (messageId === programmerModeCard.messageId) { programmingModeCardActive = false + promptOptionAcknowledged = true + } else if (messageId === deprecationCard.messageId) { + deprecationCardActive = false + promptOptionAcknowledged = true + } + + if (promptOptionAcknowledged) { messager.onChatPromptOptionAcknowledged(messageId) - // Update the tab defaults to hide the programmer mode card for new tabs + // Update the tab defaults to hide acknowledged cards for new tabs. mynahUi.updateTabDefaults({ store: { - chatItems: tabFactory.getChatItems(true, false), + chatItems: tabFactory.getChatItems(true, programmingModeCardActive, deprecationCardActive), }, }) } @@ -823,7 +840,7 @@ export const createMynahUi = ( isSelected: true, store: { ...tabFactory.createTab(disclaimerCardActive), - chatItems: tabFactory.getChatItems(true, programmingModeCardActive), + chatItems: tabFactory.getChatItems(true, programmingModeCardActive, deprecationCardActive), }, }, }, @@ -1404,7 +1421,12 @@ ${params.message}`, const tabId = createTabId(true) if (tabId) { mynahUi.updateStore(tabId, { - chatItems: tabFactory.getChatItems(messages ? false : true, programmingModeCardActive, messages), + chatItems: tabFactory.getChatItems( + messages ? false : true, + programmingModeCardActive, + deprecationCardActive, + messages + ), // onTabAdd suppresses the welcome splash whenever // openTabKey is true (which createTabId(true) sets), so // re-establish it here for the no-messages case so a diff --git a/chat-client/src/client/tabs/tabFactory.test.ts b/chat-client/src/client/tabs/tabFactory.test.ts index 815e81a22e..bfcee87d2f 100644 --- a/chat-client/src/client/tabs/tabFactory.test.ts +++ b/chat-client/src/client/tabs/tabFactory.test.ts @@ -1,8 +1,10 @@ import { ChatHistory } from '../features/history' import { TabFactory } from './tabFactory' import * as assert from 'assert' -import { pairProgrammingPromptInput } from '../texts/pairProgramming' +import { pairProgrammingPromptInput, programmerModeCard } from '../texts/pairProgramming' import { modelSelection } from '../texts/modelSelection' +import { deprecationCard } from '../texts/deprecation' +import { ChatMessage } from '@aws/language-server-runtimes-types' describe('tabFactory', () => { describe('getDefaultTabData', () => { @@ -121,4 +123,54 @@ describe('tabFactory', () => { assert.deepStrictEqual(result.promptInputOptions, []) }) }) + + describe('getChatItems', () => { + it('shows the deprecation card in a new chat when it is active', () => { + const tabFactory = new TabFactory({}) + + const result = tabFactory.getChatItems(true, false, true) + + assert.deepStrictEqual(result, [deprecationCard]) + }) + + it('shows the deprecation card before the agentic feature card', () => { + const tabFactory = new TabFactory({}) + tabFactory.enableAgenticMode() + + const result = tabFactory.getChatItems(true, true, true) + + assert.deepStrictEqual(result, [deprecationCard, programmerModeCard]) + }) + + it('hides the deprecation card after it has been acknowledged', () => { + const tabFactory = new TabFactory({}) + tabFactory.enableAgenticMode() + + const result = tabFactory.getChatItems(true, true, false) + + assert.deepStrictEqual(result, [programmerModeCard]) + }) + + it('does not add welcome cards to restored chats', () => { + const messages: ChatMessage[] = [ + { + body: 'Restored response', + type: 'answer', + }, + ] + const tabFactory = new TabFactory({}) + + const result = tabFactory.getChatItems(false, true, true, messages) + + assert.equal(result.length, 1) + assert.equal(result[0].body, 'Restored response') + assert.equal( + result.some( + item => + item.messageId === deprecationCard.messageId || item.messageId === programmerModeCard.messageId + ), + false + ) + }) + }) }) diff --git a/chat-client/src/client/tabs/tabFactory.ts b/chat-client/src/client/tabs/tabFactory.ts index 9414fc49ff..317c912ed9 100644 --- a/chat-client/src/client/tabs/tabFactory.ts +++ b/chat-client/src/client/tabs/tabFactory.ts @@ -13,6 +13,7 @@ import { pairProgrammingPromptInput, programmerModeCard } from '../texts/pairPro import { modelSelection } from '../texts/modelSelection' import { getWelcomeTabHeader } from '../texts/welcome' import { chatMessageToChatItem } from '../utils' +import { deprecationCard } from '../texts/deprecation' export type DefaultTabData = MynahUIDataModel @@ -64,14 +65,16 @@ export class TabFactory { public getChatItems( needWelcomeMessages: boolean, pairProgrammingCardActive: boolean, + deprecationCardActive: boolean, chatMessages?: ChatMessage[] ): ChatItem[] { return [ ...(this.bannerMessage ? [this.getBannerMessage() as ChatItem] : []), ...(needWelcomeMessages - ? this.agenticMode && pairProgrammingCardActive - ? [programmerModeCard] - : [] + ? [ + ...(deprecationCardActive ? [deprecationCard] : []), + ...(this.agenticMode && pairProgrammingCardActive ? [programmerModeCard] : []), + ] : chatMessages ? chatMessages.map(msg => chatMessageToChatItem(msg, this.agenticMode)) : []), diff --git a/chat-client/src/client/texts/deprecation.test.ts b/chat-client/src/client/texts/deprecation.test.ts new file mode 100644 index 0000000000..85acb8d9e9 --- /dev/null +++ b/chat-client/src/client/texts/deprecation.test.ts @@ -0,0 +1,22 @@ +import * as assert from 'assert' +import { ChatItemType } from '@aws/mynah-ui' +import { deprecationCard } from './deprecation' + +describe('deprecationCard', () => { + it('uses the approved copy and warning presentation', () => { + assert.equal(deprecationCard.type, ChatItemType.ANSWER) + assert.equal(deprecationCard.messageId, 'client-deprecation-notice') + assert.equal(deprecationCard.title, 'IMPORTANT') + assert.equal(deprecationCard.status, 'warning') + assert.equal(deprecationCard.border, true) + assert.equal(deprecationCard.fullWidth, true) + assert.equal(deprecationCard.canBeDismissed, true) + assert.equal(deprecationCard.header?.icon, 'warning') + assert.equal(deprecationCard.header?.iconStatus, 'warning') + assert.equal(deprecationCard.header?.body, '### Deprecation notice') + assert.equal( + deprecationCard.body, + 'On April 30, 2027, AWS will discontinue support for Amazon Q Developer IDE plugins. For capabilities similar to Amazon Q Developer IDE plugins, explore Kiro to access the latest models and features, including agentic coding, chat and MCP support.\n\n[Learn more](https://kiro.dev)' + ) + }) +}) diff --git a/chat-client/src/client/texts/deprecation.ts b/chat-client/src/client/texts/deprecation.ts new file mode 100644 index 0000000000..a7224eb8ff --- /dev/null +++ b/chat-client/src/client/texts/deprecation.ts @@ -0,0 +1,17 @@ +import { ChatItem, ChatItemType } from '@aws/mynah-ui' + +export const deprecationCard: ChatItem = { + type: ChatItemType.ANSWER, + messageId: 'client-deprecation-notice', + title: 'IMPORTANT', + status: 'warning', + border: true, + fullWidth: true, + canBeDismissed: true, + header: { + icon: 'warning', + iconStatus: 'warning', + body: '### Deprecation notice', + }, + body: 'On April 30, 2027, AWS will discontinue support for Amazon Q Developer IDE plugins. For capabilities similar to Amazon Q Developer IDE plugins, explore Kiro to access the latest models and features, including agentic coding, chat and MCP support.\n\n[Learn more](https://kiro.dev)', +} From 53806c4e7853ba57b082cd92fc68537bcaf1cec2 Mon Sep 17 00:00:00 2001 From: Aarush Arora Date: Wed, 16 Sep 2026 12:32:50 -0700 Subject: [PATCH 2/3] fix(chat-client): replace agentic feature card --- chat-client/src/client/chat.ts | 4 +-- chat-client/src/client/mynahUi.test.ts | 29 +++------------ chat-client/src/client/mynahUi.ts | 35 ++++--------------- .../src/client/tabs/tabFactory.test.ts | 21 +++++------ chat-client/src/client/tabs/tabFactory.ts | 10 +++--- .../src/client/texts/pairProgramming.test.ts | 20 +---------- .../src/client/texts/pairProgramming.ts | 14 -------- 7 files changed, 28 insertions(+), 105 deletions(-) diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index de68ce62d4..4c31a0a2cd 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -129,6 +129,7 @@ const getDefaultTabConfig = (agenticMode?: boolean) => { type ChatClientConfig = Pick & { disclaimerAcknowledged?: boolean + // Retained for compatibility with clients that still send the former feature-card state. pairProgrammingAcknowledged?: boolean deprecationNoticeAcknowledged?: boolean agenticMode?: boolean @@ -357,7 +358,7 @@ export const createChat = ( // that tab does not have banner message, which arrives in ChatOptions above. const store = mynahUi.getTabData(tabFactory.initialTabId)?.getStore() || {} const chatItems = store.chatItems || [] - const updatedInitialItems = tabFactory.getChatItems(false, false, false, chatItems as ChatMessage[]) + const updatedInitialItems = tabFactory.getChatItems(false, false, chatItems as ChatMessage[]) // First clear the tab, so that messages are not appended https://github.com/aws/mynah-ui/blob/38608dff905b3790d85c73e2911ec7071c8a8cdf/docs/USAGE.md#using-updatestore-function mynahUi.updateStore(tabFactory.initialTabId, { @@ -572,7 +573,6 @@ export const createChat = ( messager, tabFactory, config?.disclaimerAcknowledged ?? false, - config?.pairProgrammingAcknowledged ?? false, config?.deprecationNoticeAcknowledged ?? false, chatClientAdapter, featureConfig, diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts index 54ba171d0e..ff6484a006 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -15,7 +15,7 @@ import { ChatItemType, MynahUI, NotificationType } from '@aws/mynah-ui' import { ChatClientAdapter } from '../contracts/chatClientAdapter' import { ChatMessage, ContextCommand, ListAvailableModelsResult } from '@aws/language-server-runtimes-types' import { ChatHistory } from './features/history' -import { pairProgrammingModeOn, pairProgrammingModeOff, programmerModeCard } from './texts/pairProgramming' +import { pairProgrammingModeOn, pairProgrammingModeOff } from './texts/pairProgramming' import { deprecationCard } from './texts/deprecation' import { strictEqual } from 'assert' @@ -92,7 +92,7 @@ describe('MynahUI', () => { createTabStub.returns({}) getChatItemsStub = sinon.stub(tabFactory, 'getChatItems') getChatItemsStub.returns([]) - const mynahUiResult = createMynahUi(messager, tabFactory, true, false, false, undefined, undefined, true) + const mynahUiResult = createMynahUi(messager, tabFactory, true, false, undefined, undefined, true) mynahUi = mynahUiResult[0] inboundChatApi = mynahUiResult[1] getSelectedTabIdStub = sinon.stub(mynahUi, 'getSelectedTabId') @@ -171,7 +171,7 @@ describe('MynahUI', () => { inboundChatApi.openTab(requestId, {}) sinon.assert.calledOnceWithExactly(createTabStub, false) - sinon.assert.calledOnceWithExactly(getChatItemsStub, true, true, true, undefined) + sinon.assert.calledOnceWithExactly(getChatItemsStub, true, true, undefined) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -202,7 +202,7 @@ describe('MynahUI', () => { }) sinon.assert.calledOnceWithExactly(createTabStub, false) - sinon.assert.calledOnceWithExactly(getChatItemsStub, false, true, true, mockMessages) + sinon.assert.calledOnceWithExactly(getChatItemsStub, false, true, mockMessages) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -761,7 +761,6 @@ describe('MynahUI', () => { tabFactory, true, true, - true, undefined, undefined, true, @@ -789,24 +788,7 @@ describe('MynahUI', () => { outboundChatApi.chatPromptOptionAcknowledged as sinon.SinonStub, deprecationCard.messageId ) - sinon.assert.calledWithExactly(getChatItemsStub, true, true, false) - sinon.assert.calledWithExactly(updateTabDefaultsSpy, { - store: { - chatItems: [], - }, - }) - }) - - it('acknowledges the agentic feature card without removing the deprecation card from future new chats', () => { - const updateTabDefaultsSpy = sinon.spy(mynahUi, 'updateTabDefaults') - - ;(mynahUi as any).props.onMessageDismiss('tab-1', programmerModeCard.messageId) - - sinon.assert.calledWithExactly( - outboundChatApi.chatPromptOptionAcknowledged as sinon.SinonStub, - programmerModeCard.messageId - ) - sinon.assert.calledWithExactly(getChatItemsStub, true, false, true) + sinon.assert.calledWithExactly(getChatItemsStub, true, false) sinon.assert.calledWithExactly(updateTabDefaultsSpy, { store: { chatItems: [], @@ -848,7 +830,6 @@ describe('withAdapter', () => { tabFactory, true, true, - true, chatClientAdapter, undefined, true diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 6a56fc3fa5..1c6e5ccc7c 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -69,7 +69,7 @@ import { toMynahIcon, } from './utils' import { ChatHistory, ChatHistoryList } from './features/history' -import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming' +import { pairProgrammingModeOff, pairProgrammingModeOn } from './texts/pairProgramming' import { deprecationCard } from './texts/deprecation' import { ContextRule, RulesList } from './features/rules' import { getModelSelectionChatItem, modelUnavailableBanner, modelThrottledBanner } from './texts/modelSelection' @@ -324,7 +324,6 @@ export const createMynahUi = ( messager: Messager, tabFactory: TabFactory, disclaimerAcknowledged: boolean, - pairProgrammingCardAcknowledged: boolean, deprecationNoticeAcknowledged: boolean, customChatClientAdapter?: ChatClientAdapter, featureConfig?: Map, @@ -333,7 +332,6 @@ export const createMynahUi = ( os?: string ): [MynahUI, InboundChatApi] => { let disclaimerCardActive = !disclaimerAcknowledged - let programmingModeCardActive = !pairProgrammingCardAcknowledged let deprecationCardActive = !deprecationNoticeAcknowledged let contextCommandGroups: ContextCommandGroups | undefined let lastFilterTabId: string | undefined @@ -437,12 +435,7 @@ export const createMynahUi = ( // We check if tabMetadata.openTabKey exists - if it does and is set to true, we skip showing welcome messages // since this indicates we're loading a previous chat session rather than starting a new one. if (!tabStore?.tabMetadata || !tabStore.tabMetadata.openTabKey) { - defaultTabConfig.chatItems = tabFactory.getChatItems( - true, - programmingModeCardActive, - deprecationCardActive, - [] - ) + defaultTabConfig.chatItems = tabFactory.getChatItems(true, deprecationCardActive, []) // Roll a fresh "Did you know?" tip for every new tab. The // mynah-ui defaults.store is built once at startup, so without // this override every new tab would inherit the same cached tip. @@ -720,23 +713,14 @@ export const createMynahUi = ( messager.onPromptInputButtonClick(payload) }, onMessageDismiss: (tabId, messageId) => { - let promptOptionAcknowledged = false - - if (messageId === programmerModeCard.messageId) { - programmingModeCardActive = false - promptOptionAcknowledged = true - } else if (messageId === deprecationCard.messageId) { + if (messageId === deprecationCard.messageId) { deprecationCardActive = false - promptOptionAcknowledged = true - } - - if (promptOptionAcknowledged) { messager.onChatPromptOptionAcknowledged(messageId) - // Update the tab defaults to hide acknowledged cards for new tabs. + // Update the tab defaults to hide the acknowledged card for new tabs. mynahUi.updateTabDefaults({ store: { - chatItems: tabFactory.getChatItems(true, programmingModeCardActive, deprecationCardActive), + chatItems: tabFactory.getChatItems(true, deprecationCardActive), }, }) } @@ -840,7 +824,7 @@ export const createMynahUi = ( isSelected: true, store: { ...tabFactory.createTab(disclaimerCardActive), - chatItems: tabFactory.getChatItems(true, programmingModeCardActive, deprecationCardActive), + chatItems: tabFactory.getChatItems(true, deprecationCardActive), }, }, }, @@ -1421,12 +1405,7 @@ ${params.message}`, const tabId = createTabId(true) if (tabId) { mynahUi.updateStore(tabId, { - chatItems: tabFactory.getChatItems( - messages ? false : true, - programmingModeCardActive, - deprecationCardActive, - messages - ), + chatItems: tabFactory.getChatItems(messages ? false : true, deprecationCardActive, messages), // onTabAdd suppresses the welcome splash whenever // openTabKey is true (which createTabId(true) sets), so // re-establish it here for the no-messages case so a diff --git a/chat-client/src/client/tabs/tabFactory.test.ts b/chat-client/src/client/tabs/tabFactory.test.ts index bfcee87d2f..5a13fddf07 100644 --- a/chat-client/src/client/tabs/tabFactory.test.ts +++ b/chat-client/src/client/tabs/tabFactory.test.ts @@ -1,7 +1,7 @@ import { ChatHistory } from '../features/history' import { TabFactory } from './tabFactory' import * as assert from 'assert' -import { pairProgrammingPromptInput, programmerModeCard } from '../texts/pairProgramming' +import { pairProgrammingPromptInput } from '../texts/pairProgramming' import { modelSelection } from '../texts/modelSelection' import { deprecationCard } from '../texts/deprecation' import { ChatMessage } from '@aws/language-server-runtimes-types' @@ -128,27 +128,27 @@ describe('tabFactory', () => { it('shows the deprecation card in a new chat when it is active', () => { const tabFactory = new TabFactory({}) - const result = tabFactory.getChatItems(true, false, true) + const result = tabFactory.getChatItems(true, true) assert.deepStrictEqual(result, [deprecationCard]) }) - it('shows the deprecation card before the agentic feature card', () => { + it('replaces the agentic feature card in agentic mode', () => { const tabFactory = new TabFactory({}) tabFactory.enableAgenticMode() - const result = tabFactory.getChatItems(true, true, true) + const result = tabFactory.getChatItems(true, true) - assert.deepStrictEqual(result, [deprecationCard, programmerModeCard]) + assert.deepStrictEqual(result, [deprecationCard]) }) it('hides the deprecation card after it has been acknowledged', () => { const tabFactory = new TabFactory({}) tabFactory.enableAgenticMode() - const result = tabFactory.getChatItems(true, true, false) + const result = tabFactory.getChatItems(true, false) - assert.deepStrictEqual(result, [programmerModeCard]) + assert.deepStrictEqual(result, []) }) it('does not add welcome cards to restored chats', () => { @@ -160,15 +160,12 @@ describe('tabFactory', () => { ] const tabFactory = new TabFactory({}) - const result = tabFactory.getChatItems(false, true, true, messages) + const result = tabFactory.getChatItems(false, true, messages) assert.equal(result.length, 1) assert.equal(result[0].body, 'Restored response') assert.equal( - result.some( - item => - item.messageId === deprecationCard.messageId || item.messageId === programmerModeCard.messageId - ), + result.some(item => item.messageId === deprecationCard.messageId), false ) }) diff --git a/chat-client/src/client/tabs/tabFactory.ts b/chat-client/src/client/tabs/tabFactory.ts index 317c912ed9..8085fe2411 100644 --- a/chat-client/src/client/tabs/tabFactory.ts +++ b/chat-client/src/client/tabs/tabFactory.ts @@ -9,7 +9,7 @@ import { import { disclaimerCard } from '../texts/disclaimer' import { ChatMessage } from '@aws/language-server-runtimes-types' import { ChatHistory } from '../features/history' -import { pairProgrammingPromptInput, programmerModeCard } from '../texts/pairProgramming' +import { pairProgrammingPromptInput } from '../texts/pairProgramming' import { modelSelection } from '../texts/modelSelection' import { getWelcomeTabHeader } from '../texts/welcome' import { chatMessageToChatItem } from '../utils' @@ -64,17 +64,15 @@ export class TabFactory { public getChatItems( needWelcomeMessages: boolean, - pairProgrammingCardActive: boolean, deprecationCardActive: boolean, chatMessages?: ChatMessage[] ): ChatItem[] { return [ ...(this.bannerMessage ? [this.getBannerMessage() as ChatItem] : []), ...(needWelcomeMessages - ? [ - ...(deprecationCardActive ? [deprecationCard] : []), - ...(this.agenticMode && pairProgrammingCardActive ? [programmerModeCard] : []), - ] + ? deprecationCardActive + ? [deprecationCard] + : [] : chatMessages ? chatMessages.map(msg => chatMessageToChatItem(msg, this.agenticMode)) : []), diff --git a/chat-client/src/client/texts/pairProgramming.test.ts b/chat-client/src/client/texts/pairProgramming.test.ts index 8181f50c8b..5ccb6d7980 100644 --- a/chat-client/src/client/texts/pairProgramming.test.ts +++ b/chat-client/src/client/texts/pairProgramming.test.ts @@ -1,26 +1,8 @@ import * as assert from 'assert' import { ChatItemType } from '@aws/mynah-ui' -import { - programmerModeCard, - pairProgrammingPromptInput, - pairProgrammingModeOn, - pairProgrammingModeOff, -} from './pairProgramming' +import { pairProgrammingPromptInput, pairProgrammingModeOn, pairProgrammingModeOff } from './pairProgramming' describe('pairProgramming', () => { - describe('programmerModeCard', () => { - it('has correct properties', () => { - assert.equal(programmerModeCard.type, ChatItemType.ANSWER) - assert.equal(programmerModeCard.title, 'NEW FEATURE') - assert.equal(programmerModeCard.messageId, 'programmerModeCardId') - assert.equal(programmerModeCard.fullWidth, true) - assert.equal(programmerModeCard.canBeDismissed, true) - assert.ok(programmerModeCard.body?.includes('Amazon Q can now help')) - assert.equal(programmerModeCard.header?.icon, 'code-block') - assert.equal(programmerModeCard.header?.iconStatus, 'primary') - }) - }) - describe('pairProgrammingPromptInput', () => { it('has correct properties', () => { assert.equal(pairProgrammingPromptInput.type, 'switch') diff --git a/chat-client/src/client/texts/pairProgramming.ts b/chat-client/src/client/texts/pairProgramming.ts index 335a37069c..a92d4bf89f 100644 --- a/chat-client/src/client/texts/pairProgramming.ts +++ b/chat-client/src/client/texts/pairProgramming.ts @@ -1,19 +1,5 @@ import { ChatItem, ChatItemFormItem, ChatItemType } from '@aws/mynah-ui' -export const programmerModeCard: ChatItem = { - type: ChatItemType.ANSWER, - title: 'NEW FEATURE', - header: { - icon: 'code-block', - iconStatus: 'primary', - body: '### An interactive, agentic coding experience', - }, - messageId: 'programmerModeCardId', - fullWidth: true, - canBeDismissed: true, - body: 'Amazon Q can now help you write, modify, and maintain code by combining the power of natural language understanding with the ability to take actions on your behalf such as directly making code changes, modifying files, and running commands.', -} - export const pairProgrammingPromptInput: ChatItemFormItem = { type: 'switch', id: 'pair-programmer-mode', From 87bf23c3e1f7d4e2c22f0886c6b62aa9597adce9 Mon Sep 17 00:00:00 2001 From: Aarush Arora Date: Wed, 16 Sep 2026 14:55:50 -0700 Subject: [PATCH 3/3] fix(chat-client): show deprecation notice once per instance --- chat-client/src/client/mynahUi.test.ts | 12 ++++++++--- chat-client/src/client/mynahUi.ts | 28 +++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts index ff6484a006..605d73a7bb 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -164,14 +164,18 @@ describe('MynahUI', () => { }) describe('openTab', () => { - it('should create a new tab with welcome messages if tabId not passed and previous messages not passed', () => { + it('should show the deprecation card while initializing the first tab', () => { + sinon.assert.calledWith(getChatItemsStub, true, true) + }) + + it('should create a new tab with welcome messages without repeating the deprecation card', () => { createTabStub.resetHistory() getChatItemsStub.resetHistory() inboundChatApi.openTab(requestId, {}) sinon.assert.calledOnceWithExactly(createTabStub, false) - sinon.assert.calledOnceWithExactly(getChatItemsStub, true, true, undefined) + sinon.assert.calledOnceWithExactly(getChatItemsStub, true, false, undefined) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -202,7 +206,7 @@ describe('MynahUI', () => { }) sinon.assert.calledOnceWithExactly(createTabStub, false) - sinon.assert.calledOnceWithExactly(getChatItemsStub, false, true, mockMessages) + sinon.assert.calledOnceWithExactly(getChatItemsStub, false, false, mockMessages) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -252,6 +256,7 @@ describe('MynahUI', () => { this.timeout(10000) // Increase timeout to 10 seconds // clear create tab stub since set up process calls it twice createTabStub.resetHistory() + getChatItemsStub.resetHistory() // Stub setTimeout to execute immediately const setTimeoutStub = sinon.stub(global, 'setTimeout').callsFake((fn: Function) => { fn() @@ -266,6 +271,7 @@ describe('MynahUI', () => { inboundChatApi.sendGenericCommand({ genericCommand, selection, tabId, triggerType }) sinon.assert.calledOnceWithExactly(createTabStub, false) + sinon.assert.calledOnceWithExactly(getChatItemsStub, true, false, []) // updateStore is called four times for a brand new tab: // 1. onTabAdd seeds the tab (chatItems + welcome tabHeaderDetails) // 2. handleChatPrompt clears the welcome splash before the first prompt diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 1c6e5ccc7c..893f2204ef 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -333,9 +333,26 @@ export const createMynahUi = ( ): [MynahUI, InboundChatApi] => { let disclaimerCardActive = !disclaimerAcknowledged let deprecationCardActive = !deprecationNoticeAcknowledged + let deprecationCardShownInCurrentInstance = false let contextCommandGroups: ContextCommandGroups | undefined let lastFilterTabId: string | undefined + const shouldShowDeprecationCard = (tabId: string): boolean => { + if (!deprecationCardActive) { + return false + } + + // Mynah initializes the first tab through both the initial data model and + // onTabAdd. Allow both writes for that tab, but suppress the card for every + // subsequent tab created in this chat-client instance. + if (deprecationCardShownInCurrentInstance && tabId !== tabFactory.initialTabId) { + return false + } + + deprecationCardShownInCurrentInstance = true + return true + } + let chatEventHandlers: ChatEventHandler = { onCodeInsertToCursorPosition( tabId, @@ -435,7 +452,7 @@ export const createMynahUi = ( // We check if tabMetadata.openTabKey exists - if it does and is set to true, we skip showing welcome messages // since this indicates we're loading a previous chat session rather than starting a new one. if (!tabStore?.tabMetadata || !tabStore.tabMetadata.openTabKey) { - defaultTabConfig.chatItems = tabFactory.getChatItems(true, deprecationCardActive, []) + defaultTabConfig.chatItems = tabFactory.getChatItems(true, shouldShowDeprecationCard(tabId), []) // Roll a fresh "Did you know?" tip for every new tab. The // mynah-ui defaults.store is built once at startup, so without // this override every new tab would inherit the same cached tip. @@ -824,7 +841,7 @@ export const createMynahUi = ( isSelected: true, store: { ...tabFactory.createTab(disclaimerCardActive), - chatItems: tabFactory.getChatItems(true, deprecationCardActive), + chatItems: tabFactory.getChatItems(true, shouldShowDeprecationCard(tabFactory.initialTabId)), }, }, }, @@ -1404,8 +1421,13 @@ ${params.message}`, const messages = params.newTabOptions?.data?.messages const tabId = createTabId(true) if (tabId) { + const needWelcomeMessages = !messages mynahUi.updateStore(tabId, { - chatItems: tabFactory.getChatItems(messages ? false : true, deprecationCardActive, messages), + chatItems: tabFactory.getChatItems( + needWelcomeMessages, + needWelcomeMessages && shouldShowDeprecationCard(tabId), + messages + ), // onTabAdd suppresses the welcome splash whenever // openTabKey is true (which createTabId(true) sets), so // re-establish it here for the no-messages case so a