Skip to content

Commit 7e37569

Browse files
authored
feat(amazonq): Add conversation persistence to agentic chat (aws#6965)
## 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.) ![Screenshot 2025-04-08 at 12 03 11 PM](https://github.com/user-attachments/assets/9b383294-5e0a-499b-a4e8-993dd73b555c) ## Note Agentic chat does not currently use this persisted history in it's context. Follow-up PR will address this issue. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent c920e68 commit 7e37569

File tree

5 files changed

+48
-4
lines changed

5 files changed

+48
-4
lines changed

packages/core/src/codewhispererChat/clients/chat/v0/chat.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export type ToolUseWithError = {
2121
toolUse: ToolUse
2222
error: Error | undefined
2323
}
24+
import { getLogger } from '../../../../shared/logger/logger'
25+
import { randomUUID } from '../../../../shared/crypto'
2426

2527
export class ChatSession {
2628
private sessionId?: string
@@ -141,6 +143,10 @@ export class ChatSession {
141143
}
142144

143145
this.sessionId = response.conversationId
146+
if (this.sessionId?.length === 0) {
147+
getLogger().debug(`Session ID: ${this.sessionId} is empty. Generating random UUID`)
148+
this.sessionId = randomUUID()
149+
}
144150

145151
UserWrittenCodeTracker.instance.onQFeatureInvoked()
146152

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,13 +1497,15 @@ export class ChatController {
14971497
}
14981498
this.telemetryHelper.recordEnterFocusConversation(triggerEvent.tabID)
14991499
this.telemetryHelper.recordStartConversation(triggerEvent, triggerPayload)
1500-
if (currentMessage) {
1500+
1501+
if (currentMessage && session.sessionIdentifier) {
15011502
chatHistory.appendUserMessage(currentMessage)
1502-
}
1503-
if (session.sessionIdentifier) {
15041503
this.chatHistoryDb.addMessage(tabID, 'cwc', session.sessionIdentifier, {
15051504
body: triggerPayload.message,
15061505
type: 'prompt' as any,
1506+
userIntent: currentMessage.userInputMessage?.userIntent,
1507+
origin: currentMessage.userInputMessage?.origin,
1508+
userInputMessageContext: currentMessage.userInputMessage?.userInputMessageContext,
15071509
})
15081510
}
15091511

packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@ export class Messenger {
379379
type: 'answer' as any,
380380
codeReference: codeReference as any,
381381
relatedContent: { title: 'Sources', content: relatedSuggestions as any },
382+
messageId: messageID,
383+
toolUses:
384+
toolUse && toolUse.input !== undefined && toolUse.input !== ''
385+
? [{ ...toolUse }]
386+
: undefined,
382387
})
383388
}
384389
if (

packages/core/src/shared/db/chatDb/chatDb.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import crypto from 'crypto'
2020
import path from 'path'
2121
import { fs } from '../../fs/fs'
22+
import { getLogger } from '../../logger/logger'
2223

2324
/**
2425
* A singleton database class that manages chat history persistence using LokiJS.
@@ -36,6 +37,7 @@ import { fs } from '../../fs/fs'
3637
export class Database {
3738
private static instance: Database | undefined = undefined
3839
private db: Loki
40+
private logger = getLogger('chatHistoryDb')
3941
/**
4042
* Keep track of which open tabs have a corresponding history entry. Maps tabIds to historyIds
4143
*/
@@ -48,6 +50,8 @@ export class Database {
4850
const workspaceId = this.getWorkspaceIdentifier()
4951
const dbName = `chat-history-${workspaceId}.json`
5052

53+
this.logger.debug(`Initializing database at ${this.dbDirectory}/${dbName}`)
54+
5155
this.db = new Loki(dbName, {
5256
adapter: new FileSystemAdapter(this.dbDirectory),
5357
autosave: true,
@@ -66,6 +70,7 @@ export class Database {
6670
}
6771

6872
setHistoryIdMapping(tabId: string, historyId: string) {
73+
this.logger.debug(`[Setting historyIdMapping: tabId=${tabId}, historyId=${historyId}`)
6974
this.historyIdMapping.set(tabId, historyId)
7075
}
7176

@@ -92,12 +97,14 @@ export class Database {
9297
}
9398

9499
// Case 4: No workspace
100+
this.logger.debug(`No workspace found, using default identifier: 'no-workspace'`)
95101
return 'no-workspace'
96102
}
97103

98104
async databaseInitialize() {
99105
let entries = this.db.getCollection(TabCollection)
100106
if (entries === null) {
107+
this.logger.info(`Creating new tabs collection`)
101108
entries = this.db.addCollection(TabCollection, {
102109
unique: ['historyId'],
103110
indices: ['updatedAt', 'isOpen'],
@@ -137,24 +144,32 @@ export class Database {
137144
if (this.initialized) {
138145
const tabCollection = this.db.getCollection<Tab>(TabCollection)
139146
const historyId = this.historyIdMapping.get(tabId)
147+
this.logger.info(`Clearing tab: tabId=${tabId}, historyId=${historyId || 'undefined'}`)
140148
if (historyId) {
141149
tabCollection.findAndRemove({ historyId })
150+
this.logger.debug(`Removed tab with historyId=${historyId} from collection`)
142151
}
143152
this.historyIdMapping.delete(tabId)
153+
this.logger.debug(`Removed tabId=${tabId} from historyIdMapping`)
144154
}
145155
}
146156

147157
updateTabOpenState(tabId: string, isOpen: boolean) {
148158
if (this.initialized) {
149159
const tabCollection = this.db.getCollection<Tab>(TabCollection)
150160
const historyId = this.historyIdMapping.get(tabId)
161+
this.logger.info(
162+
`Updating tab open state: tabId=${tabId}, historyId=${historyId || 'undefined'}, isOpen=${isOpen}`
163+
)
151164
if (historyId) {
152165
tabCollection.findAndUpdate({ historyId }, (tab: Tab) => {
153166
tab.isOpen = isOpen
154167
return tab
155168
})
169+
this.logger.debug(`Updated tab open state in collection`)
156170
if (!isOpen) {
157171
this.historyIdMapping.delete(tabId)
172+
this.logger.debug(`Removed tabId=${tabId} from historyIdMapping`)
158173
}
159174
}
160175
}
@@ -164,9 +179,11 @@ export class Database {
164179
let searchResults: DetailedListItemGroup[] = []
165180
if (this.initialized) {
166181
if (!filter) {
182+
this.logger.info(`Empty search filter, returning all history`)
167183
return this.getHistory()
168184
}
169185

186+
this.logger.info(`Searching messages with filter: "${filter}"`)
170187
const searchTermLower = filter.toLowerCase()
171188
const tabCollection = this.db.getCollection<Tab>(TabCollection)
172189
const tabs = tabCollection.find()
@@ -177,9 +194,11 @@ export class Database {
177194
})
178195
})
179196
})
197+
this.logger.info(`Found ${filteredTabs.length} matching tabs`)
180198
searchResults = groupTabsByDate(filteredTabs)
181199
}
182200
if (searchResults.length === 0) {
201+
this.logger.info(`No search results found, returning default message`)
183202
searchResults = [{ children: [{ description: 'No matches found' }] }]
184203
}
185204
return searchResults
@@ -194,6 +213,9 @@ export class Database {
194213
if (this.initialized) {
195214
const tabCollection = this.db.getCollection<Tab>(TabCollection)
196215
const historyId = this.historyIdMapping.get(tabId)
216+
this.logger.info(
217+
`Getting messages: tabId=${tabId}, historyId=${historyId || 'undefined'}, numMessages=${numMessages || 'all'}`
218+
)
197219
const tabData = historyId ? tabCollection.findOne({ historyId }) : undefined
198220
if (tabData) {
199221
const allMessages = tabData.conversations.flatMap((conversation: Conversation) => conversation.messages)
@@ -210,6 +232,7 @@ export class Database {
210232
if (this.initialized) {
211233
const tabCollection = this.db.getCollection<Tab>(TabCollection)
212234
const tabs = tabCollection.find()
235+
this.logger.debug(`Getting history from ${tabs.length} tabs`)
213236
return groupTabsByDate(tabs)
214237
}
215238
return []
@@ -218,6 +241,7 @@ export class Database {
218241
deleteHistory(historyId: string) {
219242
if (this.initialized) {
220243
const tabCollection = this.db.getCollection<Tab>(TabCollection)
244+
this.logger.info(`Deleting history: historyId=${historyId}`)
221245
tabCollection.findAndRemove({ historyId })
222246
const tabId = this.getOpenTabId(historyId)
223247
if (tabId) {
@@ -229,23 +253,29 @@ export class Database {
229253
addMessage(tabId: string, tabType: TabType, conversationId: string, message: Message) {
230254
if (this.initialized) {
231255
const tabCollection = this.db.getCollection<Tab>(TabCollection)
256+
this.logger.info(`Adding message: tabId=${tabId}, tabType=${tabType}, conversationId=${conversationId}`)
232257

233258
let historyId = this.historyIdMapping.get(tabId)
234259

235260
if (!historyId) {
236261
historyId = crypto.randomUUID()
262+
this.logger.debug(`No historyId found, creating new one: ${historyId}`)
237263
this.setHistoryIdMapping(tabId, historyId)
238264
}
239265

240266
const tabData = historyId ? tabCollection.findOne({ historyId }) : undefined
241267
const tabTitle =
242-
(message.type === ('prompt' as ChatItemType) ? message.body : tabData?.title) || 'Amazon Q Chat'
268+
message.type === ('prompt' as ChatItemType) && message.body.trim().length > 0
269+
? message.body
270+
: tabData?.title || 'Amazon Q Chat'
243271
if (tabData) {
272+
this.logger.info(`Found existing tab data, updating conversations`)
244273
tabData.conversations = updateOrCreateConversation(tabData.conversations, conversationId, message)
245274
tabData.updatedAt = new Date()
246275
tabData.title = tabTitle
247276
tabCollection.update(tabData)
248277
} else {
278+
this.logger.info(`No existing tab data, creating new tab entry`)
249279
tabCollection.insert({
250280
historyId,
251281
updatedAt: new Date(),

packages/core/src/shared/logger/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type LogTopic =
2121
| 'executeBash'
2222
| 'listDirectory'
2323
| 'chatStream'
24+
| 'chatHistoryDb'
2425
| 'unknown'
2526

2627
class ErrorLog {

0 commit comments

Comments
 (0)