Skip to content

Commit 2da999c

Browse files
authored
feat(amazonq): conversation persistence, view/search chat history #6893
## Problem Users lose all chats when they close VSCode, and there's no way to browse through chat history. Users also cant export their conversations to an easily shareable format. ## Solution Automatically persist conversations to JSON files in `~/.aws/amazonq/history`, one for each workspace where Amazon Q chats occur. Add chat history and chat export buttons to top of Amazon Q toolbar. Clicking on the chat history button allows users to browse and search through chat history. Users click on an old conversation to open it back up (currently open conversations are in bold). Clicking on chat export button allows users to save chat transcript as a markdown or html. Note: persistence + history is only for Q Chat Tabs (not /dev, /doc, /transform, etc.)
1 parent ed2d1b0 commit 2da999c

File tree

23 files changed

+1308
-17
lines changed

23 files changed

+1308
-17
lines changed

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q chat: View and search chat history"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q chat: Automatically persist chats between IDE sessions"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q chat: Click share icon to export chat to Markdown or HTML"
4+
}

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@
455455
"@types/js-yaml": "^4.0.5",
456456
"@types/jsdom": "^21.1.6",
457457
"@types/lodash": "^4.14.180",
458+
"@types/lokijs": "^1.5.14",
458459
"@types/markdown-it": "^13.0.2",
459460
"@types/mime-types": "^2.1.4",
460461
"@types/mocha": "^10.0.6",
@@ -555,6 +556,7 @@
555556
"js-yaml": "^4.1.0",
556557
"jsonc-parser": "^3.2.0",
557558
"lodash": "^4.17.21",
559+
"lokijs": "^1.5.12",
558560
"markdown-it": "^13.0.2",
559561
"mime-types": "^2.1.32",
560562
"node-fetch": "^2.7.0",

packages/core/src/amazonq/webview/ui/apps/baseConnector.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ChatItem, ChatItemAction, ChatItemType, FeedbackPayload, QuickActionCommand } from '@aws/mynah-ui'
6+
import {
7+
ChatItem,
8+
ChatItemAction,
9+
ChatItemType,
10+
DetailedList,
11+
FeedbackPayload,
12+
QuickActionCommand,
13+
} from '@aws/mynah-ui'
714
import { ExtensionMessage } from '../commands'
815
import { CodeReference } from './amazonqCommonsConnector'
916
import { TabOpenType, TabsStorage, TabType } from '../storages/tabsStorage'
1017
import { FollowUpGenerator } from '../followUps/generator'
1118
import { CWCChatItem } from '../connector'
19+
import { DetailedListSheetProps } from '@aws/mynah-ui/dist/components/detailed-list/detailed-list-sheet'
20+
import { DetailedListConnector, DetailedListType } from '../detailedList/detailedListConnector'
1221

1322
interface ChatPayload {
1423
chatMessage: string
@@ -23,6 +32,15 @@ export interface BaseConnectorProps {
2332
onError: (tabID: string, message: string, title: string) => void
2433
onWarning: (tabID: string, message: string, title: string) => void
2534
onOpenSettingsMessage: (tabID: string) => void
35+
onNewTab: (tabType: TabType, chats?: ChatItem[]) => string | undefined
36+
onOpenDetailedList: (data: DetailedListSheetProps) => {
37+
update: (data: DetailedList) => void
38+
close: () => void
39+
changeTarget: (direction: 'up' | 'down', snapOnLastAndFirst?: boolean) => void
40+
getTargetElementId: () => string | undefined
41+
}
42+
onSelectTab: (tabID: string, eventID: string) => void
43+
onExportChat: (tabId: string, format: 'html' | 'markdown') => string
2644
tabsStorage: TabsStorage
2745
}
2846

@@ -32,8 +50,13 @@ export abstract class BaseConnector {
3250
protected readonly onWarning
3351
protected readonly onChatAnswerReceived
3452
protected readonly onOpenSettingsMessage
53+
protected readonly onNewTab
54+
protected readonly onOpenDetailedList
55+
protected readonly onExportChat
56+
protected readonly onSelectTab
3557
protected readonly followUpGenerator: FollowUpGenerator
3658
protected readonly tabsStorage
59+
protected historyConnector
3760

3861
abstract getTabType(): TabType
3962

@@ -43,8 +66,17 @@ export abstract class BaseConnector {
4366
this.onWarning = props.onWarning
4467
this.onError = props.onError
4568
this.onOpenSettingsMessage = props.onOpenSettingsMessage
69+
this.onNewTab = props.onNewTab
4670
this.tabsStorage = props.tabsStorage
71+
this.onOpenDetailedList = props.onOpenDetailedList
72+
this.onExportChat = props.onExportChat
73+
this.onSelectTab = props.onSelectTab
4774
this.followUpGenerator = new FollowUpGenerator()
75+
this.historyConnector = new DetailedListConnector(
76+
DetailedListType.history,
77+
this.sendMessageToExtension,
78+
this.onOpenDetailedList
79+
)
4880
}
4981

5082
onResponseBodyLinkClick = (tabID: string, messageId: string, link: string): void => {
@@ -296,5 +328,53 @@ export abstract class BaseConnector {
296328
await this.processOpenSettingsMessage(messageData)
297329
return
298330
}
331+
332+
if (messageData.type === 'restoreTabMessage') {
333+
const newTabId = this.onNewTab(messageData.tabType, messageData.chats)
334+
this.sendMessageToExtension({
335+
command: 'tab-restored',
336+
historyId: messageData.historyId,
337+
newTabId,
338+
tabType: this.getTabType(),
339+
exportTab: messageData.exportTab,
340+
})
341+
return
342+
}
343+
344+
if (messageData.type === 'updateDetailedListMessage') {
345+
if (messageData.listType === DetailedListType.history) {
346+
this.historyConnector.updateList(messageData.detailedList)
347+
}
348+
return
349+
}
350+
351+
if (messageData.type === 'closeDetailedListMessage') {
352+
if (messageData.listType === DetailedListType.history) {
353+
this.historyConnector.closeList()
354+
}
355+
return
356+
}
357+
358+
if (messageData.type === 'openDetailedListMessage') {
359+
if (messageData.listType === DetailedListType.history) {
360+
this.historyConnector.openList(messageData)
361+
}
362+
return
363+
}
364+
365+
if (messageData.type === 'exportChatMessage') {
366+
const serializedChat = this.onExportChat(messageData.tabID, messageData.format)
367+
this.sendMessageToExtension({
368+
command: 'save-chat',
369+
uri: messageData.uri,
370+
serializedChat,
371+
tabType: 'cwc',
372+
})
373+
return
374+
}
375+
376+
if (messageData.type === 'selectTabMessage') {
377+
this.onSelectTab(messageData.tabID, messageData.eventID)
378+
}
299379
}
300380
}

packages/core/src/amazonq/webview/ui/apps/docChatConnector.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export interface ConnectorProps extends BaseConnectorProps {
2323
onUpdatePromptProgress: (tabID: string, progressField: ProgressField) => void
2424
onChatInputEnabled: (tabID: string, enabled: boolean) => void
2525
onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void
26-
onNewTab: (tabType: TabType) => void
2726
}
2827

2928
export class Connector extends BaseConnector {
@@ -32,7 +31,6 @@ export class Connector extends BaseConnector {
3231
private readonly updatePlaceholder
3332
private readonly chatInputEnabled
3433
private readonly onUpdateAuthentication
35-
private readonly onNewTab
3634
private readonly updatePromptProgress
3735

3836
override getTabType(): TabType {
@@ -46,7 +44,6 @@ export class Connector extends BaseConnector {
4644
this.updatePlaceholder = props.onUpdatePlaceholder
4745
this.chatInputEnabled = props.onChatInputEnabled
4846
this.onUpdateAuthentication = props.onUpdateAuthentication
49-
this.onNewTab = props.onNewTab
5047
this.updatePromptProgress = props.onUpdatePromptProgress
5148
}
5249

packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export interface ConnectorProps extends BaseConnectorProps {
3030
onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void
3131
onChatInputEnabled: (tabID: string, enabled: boolean) => void
3232
onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void
33-
onNewTab: (tabType: TabType) => void
3433
}
3534

3635
export class Connector extends BaseConnector {
@@ -40,7 +39,6 @@ export class Connector extends BaseConnector {
4039
private readonly updatePlaceholder
4140
private readonly chatInputEnabled
4241
private readonly onUpdateAuthentication
43-
private readonly onNewTab
4442

4543
override getTabType(): TabType {
4644
return 'featuredev'
@@ -53,7 +51,6 @@ export class Connector extends BaseConnector {
5351
this.updatePlaceholder = props.onUpdatePlaceholder
5452
this.chatInputEnabled = props.onChatInputEnabled
5553
this.onUpdateAuthentication = props.onUpdateAuthentication
56-
this.onNewTab = props.onNewTab
5754
this.onChatAnswerUpdated = props.onChatAnswerUpdated
5855
}
5956

packages/core/src/amazonq/webview/ui/commands.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,12 @@ type MessageCommand =
4444
| 'update-welcome-count'
4545
| 'quick-command-group-action-click'
4646
| 'context-selected'
47+
| 'tab-restored'
48+
| 'tab-bar-button-clicked'
49+
| 'export-chat'
50+
| 'save-chat'
51+
| 'detailed-list-filter-change'
52+
| 'detailed-list-item-select'
53+
| 'detailed-list-action-click'
4754

4855
export type ExtensionMessage = Record<string, any> & { command: MessageCommand }

packages/core/src/amazonq/webview/ui/connector.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
QuickActionCommand,
1717
ChatItemFormItem,
1818
ChatItemButton,
19+
DetailedList,
1920
} from '@aws/mynah-ui'
2021
import { Connector as CWChatConnector } from './apps/cwChatConnector'
2122
import { Connector as FeatureDevChatConnector } from './apps/featureDevChatConnector'
@@ -30,6 +31,7 @@ import { WelcomeFollowupType } from './apps/amazonqCommonsConnector'
3031
import { AuthFollowUpType } from './followUps/generator'
3132
import { DiffTreeFileInfo } from './diffTree/types'
3233
import { UserIntent } from '@amzn/codewhisperer-streaming'
34+
import { DetailedListSheetProps } from '@aws/mynah-ui/dist/components/detailed-list/detailed-list-sheet'
3335

3436
export interface CodeReference {
3537
licenseName?: string
@@ -94,7 +96,7 @@ export interface ConnectorProps {
9496
onUpdatePromptProgress: (tabID: string, progressField: ProgressField) => void
9597
onChatInputEnabled: (tabID: string, enabled: boolean) => void
9698
onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void
97-
onNewTab: (tabType: TabType) => void
99+
onNewTab: (tabType: TabType, chats?: ChatItem[]) => string | undefined
98100
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void
99101
handleCommand: (chatPrompt: ChatPrompt, tabId: string) => void
100102
sendStaticMessages: (tabID: string, messages: ChatItem[]) => void
@@ -106,6 +108,14 @@ export interface ConnectorProps {
106108
title?: string,
107109
description?: string
108110
) => void
111+
onOpenDetailedList: (data: DetailedListSheetProps) => {
112+
update: (data: DetailedList) => void
113+
close: () => void
114+
changeTarget: (direction: 'up' | 'down', snapOnLastAndFirst?: boolean) => void
115+
getTargetElementId: () => string | undefined
116+
}
117+
onSelectTab: (tabID: string, eventID: string) => void
118+
onExportChat: (tabID: string, format: 'markdown' | 'html') => string
109119
tabsStorage: TabsStorage
110120
}
111121

@@ -291,6 +301,7 @@ export class Connector {
291301
this.tabsStorage.updateTabLastCommand(messageData.tabID, '')
292302
}
293303

304+
// Run when user opens new tab in UI
294305
onTabAdd = (tabID: string): void => {
295306
this.tabsStorage.addTab({
296307
id: tabID,
@@ -684,6 +695,16 @@ export class Connector {
684695
return false
685696
}
686697

698+
onTabBarButtonClick = async (tabId: string, buttonId: string, eventId?: string) => {
699+
this.sendMessageToExtension({
700+
command: 'tab-bar-button-clicked',
701+
buttonId,
702+
type: '',
703+
tabID: tabId,
704+
tabType: 'cwc',
705+
})
706+
}
707+
687708
onCustomFormAction = (
688709
tabId: string,
689710
messageId: string | undefined,

0 commit comments

Comments
 (0)