Skip to content

Commit 63fd2dc

Browse files
authored
feat(chat-client): implement export conversation flow (#944)
* Added Chat Export functionality to go from Chat Client to Language Server. Q Chat server is controlling the UX flow and contains business logic for export functionality. Using new protocol methods to implement Export feature flow feat: protocol extensions for chat tab actions and export features language-server-runtimes#433. * Export feature is conditionally enabled by Q chat server if client signals support for initializationOptions?.aws?.awsClientCapabilities?.window?.showSaveFileDialog feature. * Extension must implement aws/showSaveFileDialog protocol and set awsClientCapabilities?.window?.showSaveFileDialog flag to true. * Update Sample VSCode extension with implementation of Show Save File dialog and added support. * Updated Runtimes dependencies to latest version.
1 parent a01262c commit 63fd2dc

File tree

23 files changed

+799
-286
lines changed

23 files changed

+799
-286
lines changed

app/aws-lsp-codewhisperer-runtimes/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"test": "node scripts/test-runner.js"
1414
},
1515
"dependencies": {
16-
"@aws/language-server-runtimes": "^0.2.56",
16+
"@aws/language-server-runtimes": "^0.2.61",
1717
"@aws/lsp-codewhisperer": "*",
1818
"copyfiles": "^2.4.1",
1919
"cross-env": "^7.0.3",

chat-client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"package": "webpack"
2222
},
2323
"dependencies": {
24-
"@aws/chat-client-ui-types": "^0.1.13",
25-
"@aws/language-server-runtimes-types": "^0.1.11",
24+
"@aws/chat-client-ui-types": "^0.1.15",
25+
"@aws/language-server-runtimes-types": "^0.1.13",
2626
"@aws/mynah-ui": "^4.30.1"
2727
},
2828
"devDependencies": {

chat-client/src/client/chat.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { injectJSDOM } from '../test/jsDomInjector'
22
// This needs to be run before all other imports so that mynah ui gets loaded inside of jsdom
33
injectJSDOM()
44

5-
import { ERROR_MESSAGE, GENERIC_COMMAND, SEND_TO_PROMPT } from '@aws/chat-client-ui-types'
5+
import { CHAT_OPTIONS, ERROR_MESSAGE, GENERIC_COMMAND, SEND_TO_PROMPT } from '@aws/chat-client-ui-types'
66
import {
77
CHAT_REQUEST_METHOD,
8+
GET_SERIALIZED_CHAT_REQUEST_METHOD,
89
OPEN_TAB_REQUEST_METHOD,
910
READY_NOTIFICATION_METHOD,
1011
TAB_ADD_NOTIFICATION_METHOD,
@@ -41,6 +42,8 @@ describe('Chat', () => {
4142

4243
beforeEach(() => {
4344
sandbox.stub(TabFactory, 'generateUniqueId').returns(initialTabId)
45+
sandbox.stub(TabFactory.prototype, 'enableHistory')
46+
sandbox.stub(TabFactory.prototype, 'enableExport')
4447

4548
clientApi = {
4649
postMessage: sandbox.stub(),
@@ -296,6 +299,69 @@ describe('Chat', () => {
296299
assert.notCalled(updateStoreStub)
297300
})
298301

302+
describe('chatOptions', () => {
303+
it('enables history and export features support', () => {
304+
const chatOptionsRequest = createInboundEvent({
305+
command: CHAT_OPTIONS,
306+
params: {
307+
history: true,
308+
export: true,
309+
},
310+
})
311+
window.dispatchEvent(chatOptionsRequest)
312+
313+
// @ts-ignore
314+
assert.called(TabFactory.prototype.enableHistory)
315+
// @ts-ignore
316+
assert.called(TabFactory.prototype.enableExport)
317+
})
318+
319+
it('does not enable history and export features support if flags are falsy', () => {
320+
const chatOptionsRequest = createInboundEvent({
321+
command: CHAT_OPTIONS,
322+
params: {
323+
history: false,
324+
export: false,
325+
},
326+
})
327+
window.dispatchEvent(chatOptionsRequest)
328+
329+
// @ts-ignore
330+
assert.notCalled(TabFactory.prototype.enableHistory)
331+
// @ts-ignore
332+
assert.notCalled(TabFactory.prototype.enableExport)
333+
})
334+
})
335+
336+
describe('onGetSerializedChat', () => {
337+
it('getSerializedChat requestId was propagated from inbound to outbound message', () => {
338+
const requestId = 'request-1234'
339+
const tabId = mynahUi.updateStore('', {})
340+
341+
const setSerializedChatEvent = createInboundEvent({
342+
command: GET_SERIALIZED_CHAT_REQUEST_METHOD,
343+
params: {
344+
tabId: tabId,
345+
format: 'markdown',
346+
},
347+
requestId: requestId,
348+
})
349+
window.dispatchEvent(setSerializedChatEvent)
350+
351+
// Verify that postMessage was called with the correct requestId
352+
assert.calledWithExactly(clientApi.postMessage, {
353+
requestId,
354+
command: GET_SERIALIZED_CHAT_REQUEST_METHOD,
355+
params: {
356+
success: true,
357+
result: sinon.match({
358+
content: sinon.match.string,
359+
}),
360+
},
361+
})
362+
})
363+
})
364+
299365
function createInboundEvent(params: any) {
300366
const event = new CustomEvent('message') as any
301367
event.data = params

chat-client/src/client/chat.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import {
4545
FeedbackParams,
4646
FileClickParams,
4747
FollowUpClickParams,
48+
GET_SERIALIZED_CHAT_REQUEST_METHOD,
49+
GetSerializedChatParams,
50+
GetSerializedChatResult,
4851
INFO_LINK_CLICK_NOTIFICATION_METHOD,
4952
InfoLinkClickParams,
5053
LINK_CLICK_NOTIFICATION_METHOD,
@@ -61,9 +64,11 @@ import {
6164
SOURCE_LINK_CLICK_NOTIFICATION_METHOD,
6265
SourceLinkClickParams,
6366
TAB_ADD_NOTIFICATION_METHOD,
67+
TAB_BAR_ACTION_REQUEST_METHOD,
6468
TAB_CHANGE_NOTIFICATION_METHOD,
6569
TAB_REMOVE_NOTIFICATION_METHOD,
6670
TabAddParams,
71+
TabBarActionParams,
6772
TabChangeParams,
6873
TabRemoveParams,
6974
} from '@aws/language-server-runtimes-types'
@@ -146,6 +151,8 @@ export const createChat = (
146151
case CONVERSATION_CLICK_REQUEST_METHOD:
147152
mynahApi.conversationClicked(message.params as ConversationClickResult)
148153
break
154+
case GET_SERIALIZED_CHAT_REQUEST_METHOD:
155+
mynahApi.getSerializedChat(message.requestId, message.params as GetSerializedChatParams)
149156
case CHAT_OPTIONS: {
150157
const params = (message as ChatOptionsMessage).params
151158
if (params?.quickActions?.quickActionsCommandGroups) {
@@ -158,10 +165,15 @@ export const createChat = (
158165
}))
159166
tabFactory.updateQuickActionCommands(quickActionCommandGroups)
160167
}
168+
161169
if (params?.history) {
162170
tabFactory.enableHistory()
163171
}
164172

173+
if (params?.export) {
174+
tabFactory.enableExport()
175+
}
176+
165177
const allExistingTabs: MynahUITabStoreModel = mynahUi.getAllTabs()
166178
for (const tabId in allExistingTabs) {
167179
mynahUi.updateStore(tabId, tabFactory.getDefaultTabData())
@@ -260,6 +272,30 @@ export const createChat = (
260272
conversationClick: (params: ConversationClickParams) => {
261273
sendMessageToClient({ command: CONVERSATION_CLICK_REQUEST_METHOD, params })
262274
},
275+
tabBarAction: (params: TabBarActionParams) => {
276+
sendMessageToClient({ command: TAB_BAR_ACTION_REQUEST_METHOD, params })
277+
},
278+
onGetSerializedChat: (requestId: string, params: GetSerializedChatResult | ErrorResult) => {
279+
if ('content' in params) {
280+
sendMessageToClient({
281+
requestId: requestId,
282+
command: GET_SERIALIZED_CHAT_REQUEST_METHOD,
283+
params: {
284+
success: true,
285+
result: params as GetSerializedChatResult,
286+
},
287+
})
288+
} else {
289+
sendMessageToClient({
290+
requestId: requestId,
291+
command: GET_SERIALIZED_CHAT_REQUEST_METHOD,
292+
params: {
293+
success: false,
294+
error: params as ErrorResult,
295+
},
296+
})
297+
}
298+
},
263299
}
264300

265301
const messager = new Messager(chatApi)

chat-client/src/client/messager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ import {
2929
FileClickParams,
3030
FilterValue,
3131
FollowUpClickParams,
32+
GetSerializedChatResult,
3233
InfoLinkClickParams,
3334
LinkClickParams,
3435
ListConversationsParams,
3536
OpenTabResult,
3637
QuickActionParams,
3738
SourceLinkClickParams,
3839
TabAddParams,
40+
TabBarActionParams,
3941
TabChangeParams,
4042
TabRemoveParams,
4143
} from '@aws/language-server-runtimes-types'
@@ -83,6 +85,8 @@ export interface OutboundChatApi {
8385
fileClick(params: FileClickParams): void
8486
listConversations(params: ListConversationsParams): void
8587
conversationClick(params: ConversationClickParams): void
88+
tabBarAction(params: TabBarActionParams): void
89+
onGetSerializedChat(requestId: string, result: GetSerializedChatResult | ErrorResult): void
8690
}
8791

8892
export class Messager {
@@ -197,4 +201,12 @@ export class Messager {
197201
onConversationClick = (conversationId: string, action?: ConversationAction): void => {
198202
this.chatApi.conversationClick({ id: conversationId, action })
199203
}
204+
205+
onTabBarAction = (params: TabBarActionParams): void => {
206+
this.chatApi.tabBarAction(params)
207+
}
208+
209+
onGetSerializedChat = (requestId: string, result: GetSerializedChatResult | ErrorResult): void => {
210+
this.chatApi.onGetSerializedChat(requestId, result)
211+
}
200212
}

chat-client/src/client/mynahUi.test.ts

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { assert } from 'sinon'
44
import { createMynahUi, InboundChatApi, handleChatPrompt, DEFAULT_HELP_PROMPT } from './mynahUi'
55
import { Messager, OutboundChatApi } from './messager'
66
import { TabFactory } from './tabs/tabFactory'
7-
import { ChatItemType, MynahUI } from '@aws/mynah-ui'
7+
import { ChatItemType, MynahUI, NotificationType } from '@aws/mynah-ui'
88
import { ChatClientAdapter } from '../contracts/chatClientAdapter'
99
import { ChatMessage } from '@aws/language-server-runtimes-types'
10+
import { ChatHistory } from './features/history'
1011

1112
describe('MynahUI', () => {
1213
let messager: Messager
@@ -23,6 +24,8 @@ describe('MynahUI', () => {
2324
let onQuickActionSpy: sinon.SinonSpy
2425
let onOpenTabSpy: sinon.SinonSpy
2526
let selectTabSpy: sinon.SinonSpy
27+
let serializeChatStub: sinon.SinonStub
28+
let notifySpy: sinon.SinonSpy
2629
const requestId = '1234'
2730

2831
beforeEach(() => {
@@ -48,6 +51,8 @@ describe('MynahUI', () => {
4851
fileClick: sinon.stub(),
4952
listConversations: sinon.stub(),
5053
conversationClick: sinon.stub(),
54+
tabBarAction: sinon.stub(),
55+
onGetSerializedChat: sinon.stub(),
5156
}
5257

5358
messager = new Messager(outboundChatApi)
@@ -66,6 +71,8 @@ describe('MynahUI', () => {
6671
updateStoreSpy = sinon.spy(mynahUi, 'updateStore')
6772
addChatItemSpy = sinon.spy(mynahUi, 'addChatItem')
6873
selectTabSpy = sinon.spy(mynahUi, 'selectTab')
74+
serializeChatStub = sinon.stub(mynahUi, 'serializeChat')
75+
notifySpy = sinon.spy(mynahUi, 'notify')
6976
})
7077

7178
afterEach(() => {
@@ -120,7 +127,7 @@ describe('MynahUI', () => {
120127
})
121128

122129
describe('openTab', () => {
123-
it('should create a new tab with welcome messages if tabId not passed and previous messages not passed', () => {
130+
it('should create a new tab with welcome messages if tabId not passed and previous messages not passed', () => {
124131
createTabStub.resetHistory()
125132

126133
inboundChatApi.openTab(requestId, {})
@@ -275,8 +282,31 @@ describe('MynahUI', () => {
275282
})
276283
})
277284

285+
describe('onTabBarButtonClick', () => {
286+
it('should list conversations when Chat History button is clicked', () => {
287+
const listConversationsSpy = sinon.spy(messager, 'onListConversations')
288+
289+
// @ts-ignore
290+
mynahUi.props.onTabBarButtonClick('tab-123', ChatHistory.TabBarButtonId)
291+
292+
sinon.assert.calledOnce(listConversationsSpy)
293+
})
294+
295+
it('should export conversation when Export button is clicked', () => {
296+
const listConversationsSpy = sinon.spy(messager, 'onTabBarAction')
297+
298+
// @ts-ignore
299+
mynahUi.props.onTabBarButtonClick('tab-123', 'export')
300+
301+
sinon.assert.calledOnceWithExactly(listConversationsSpy, {
302+
tabId: 'tab-123',
303+
action: 'export',
304+
})
305+
})
306+
})
307+
278308
describe('conversationClicked result', () => {
279-
it('should list conversarions if successfully deleted conversation', () => {
309+
it('should list conversations if successfully deleted conversation', () => {
280310
const listConversationsSpy = sinon.spy(messager, 'onListConversations')
281311

282312
// Successful conversation deletion
@@ -301,6 +331,40 @@ describe('MynahUI', () => {
301331
sinon.assert.neverCalledWith(listConversationsSpy)
302332
})
303333
})
334+
335+
describe('getSerializedChat', () => {
336+
it('should return serialized chat content for supported formats', () => {
337+
const onGetSerializedChatSpy = sinon.spy(messager, 'onGetSerializedChat')
338+
serializeChatStub.returns('Test Serialized Chat')
339+
340+
inboundChatApi.getSerializedChat(requestId, {
341+
format: 'markdown',
342+
tabId: 'tab-1',
343+
})
344+
345+
sinon.assert.calledWith(onGetSerializedChatSpy, requestId, { content: 'Test Serialized Chat' })
346+
})
347+
348+
it('should show an error if requested format is not supported', () => {
349+
const onGetSerializedChatSpy = sinon.spy(messager, 'onGetSerializedChat')
350+
serializeChatStub.returns('Test Serialized Chat')
351+
352+
inboundChatApi.getSerializedChat(requestId, {
353+
// @ts-ignore
354+
format: 'unsupported-format',
355+
tabId: 'tab-1',
356+
})
357+
358+
sinon.assert.calledWith(onGetSerializedChatSpy, requestId, {
359+
type: 'InvalidRequest',
360+
message: 'Failed to get serialized chat content, unsupported-format is not supported',
361+
})
362+
sinon.assert.calledWith(notifySpy, {
363+
content: `Failed to export chat`,
364+
type: NotificationType.ERROR,
365+
})
366+
})
367+
})
304368
})
305369

306370
describe('withAdapter', () => {

0 commit comments

Comments
 (0)