diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index 8d52b0b345..4c31a0a2cd 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -129,7 +129,9 @@ 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 modelSelectionEnabled?: boolean stringOverrides?: Partial @@ -571,7 +573,7 @@ export const createChat = ( messager, 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..605d73a7bb 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -16,6 +16,7 @@ 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 { 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, undefined, undefined, true) mynahUi = mynahUiResult[0] inboundChatApi = mynahUiResult[1] getSelectedTabIdStub = sinon.stub(mynahUi, 'getSelectedTabId') @@ -163,7 +164,11 @@ 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() @@ -251,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() @@ -265,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 @@ -776,6 +783,25 @@ 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, false) + sinon.assert.calledWithExactly(updateTabDefaultsSpy, { + store: { + chatItems: [], + }, + }) + }) + }) }) describe('withAdapter', () => { diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 5f24c71e45..893f2204ef 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -69,7 +69,8 @@ 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' import { getWelcomeTabHeader } from './texts/welcome' @@ -323,7 +324,7 @@ export const createMynahUi = ( messager: Messager, tabFactory: TabFactory, disclaimerAcknowledged: boolean, - pairProgrammingCardAcknowledged: boolean, + deprecationNoticeAcknowledged: boolean, customChatClientAdapter?: ChatClientAdapter, featureConfig?: Map, agenticMode?: boolean, @@ -331,10 +332,27 @@ export const createMynahUi = ( os?: string ): [MynahUI, InboundChatApi] => { let disclaimerCardActive = !disclaimerAcknowledged - let programmingModeCardActive = !pairProgrammingCardAcknowledged + 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, @@ -434,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, programmingModeCardActive, []) + 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. @@ -712,14 +730,14 @@ export const createMynahUi = ( messager.onPromptInputButtonClick(payload) }, onMessageDismiss: (tabId, messageId) => { - if (messageId === programmerModeCard.messageId) { - programmingModeCardActive = false + if (messageId === deprecationCard.messageId) { + deprecationCardActive = false messager.onChatPromptOptionAcknowledged(messageId) - // Update the tab defaults to hide the programmer mode card for new tabs + // Update the tab defaults to hide the acknowledged card for new tabs. mynahUi.updateTabDefaults({ store: { - chatItems: tabFactory.getChatItems(true, false), + chatItems: tabFactory.getChatItems(true, deprecationCardActive), }, }) } @@ -823,7 +841,7 @@ export const createMynahUi = ( isSelected: true, store: { ...tabFactory.createTab(disclaimerCardActive), - chatItems: tabFactory.getChatItems(true, programmingModeCardActive), + chatItems: tabFactory.getChatItems(true, shouldShowDeprecationCard(tabFactory.initialTabId)), }, }, }, @@ -1403,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, programmingModeCardActive, 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 diff --git a/chat-client/src/client/tabs/tabFactory.test.ts b/chat-client/src/client/tabs/tabFactory.test.ts index 815e81a22e..5a13fddf07 100644 --- a/chat-client/src/client/tabs/tabFactory.test.ts +++ b/chat-client/src/client/tabs/tabFactory.test.ts @@ -3,6 +3,8 @@ import { TabFactory } from './tabFactory' import * as assert from 'assert' import { pairProgrammingPromptInput } 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,51 @@ 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, true) + + assert.deepStrictEqual(result, [deprecationCard]) + }) + + it('replaces the agentic feature card in agentic mode', () => { + const tabFactory = new TabFactory({}) + tabFactory.enableAgenticMode() + + const result = tabFactory.getChatItems(true, true) + + 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, false) + + assert.deepStrictEqual(result, []) + }) + + 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, messages) + + assert.equal(result.length, 1) + assert.equal(result[0].body, 'Restored response') + assert.equal( + 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 9414fc49ff..8085fe2411 100644 --- a/chat-client/src/client/tabs/tabFactory.ts +++ b/chat-client/src/client/tabs/tabFactory.ts @@ -9,10 +9,11 @@ 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' +import { deprecationCard } from '../texts/deprecation' export type DefaultTabData = MynahUIDataModel @@ -63,14 +64,14 @@ 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] : [] : 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)', +} 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',