Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion chat-client/src/client/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ const getDefaultTabConfig = (agenticMode?: boolean) => {

type ChatClientConfig = Pick<MynahUIDataModel, 'quickActionCommands'> & {
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<ConfigTexts>
Expand Down Expand Up @@ -571,7 +573,7 @@ export const createChat = (
messager,
tabFactory,
config?.disclaimerAcknowledged ?? false,
config?.pairProgrammingAcknowledged ?? false,
config?.deprecationNoticeAcknowledged ?? false,
chatClientAdapter,
featureConfig,
!!config?.agenticMode,
Expand Down
30 changes: 28 additions & 2 deletions chat-client/src/client/mynahUi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down
43 changes: 33 additions & 10 deletions chat-client/src/client/mynahUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -323,18 +324,35 @@ export const createMynahUi = (
messager: Messager,
tabFactory: TabFactory,
disclaimerAcknowledged: boolean,
pairProgrammingCardAcknowledged: boolean,
deprecationNoticeAcknowledged: boolean,
customChatClientAdapter?: ChatClientAdapter,
featureConfig?: Map<string, any>,
agenticMode?: boolean,
stringOverrides?: Partial<ConfigTexts>,
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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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),
},
})
}
Expand Down Expand Up @@ -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)),
},
},
},
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions chat-client/src/client/tabs/tabFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
)
})
})
})
9 changes: 5 additions & 4 deletions chat-client/src/client/tabs/tabFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down
22 changes: 22 additions & 0 deletions chat-client/src/client/texts/deprecation.test.ts
Original file line number Diff line number Diff line change
@@ -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)'
)
})
})
17 changes: 17 additions & 0 deletions chat-client/src/client/texts/deprecation.ts
Original file line number Diff line number Diff line change
@@ -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)',
}
20 changes: 1 addition & 19 deletions chat-client/src/client/texts/pairProgramming.test.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
14 changes: 0 additions & 14 deletions chat-client/src/client/texts/pairProgramming.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
Loading