packages/core/src/codewhispererChat/controllers/chat/tabBarController.ts (153 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import fs from '../../../shared/fs/fs'
import {
DetailedListActionClickMessage,
DetailedListFilterChangeMessage,
DetailedListItemSelectMessage,
} from '../../view/connector/connector'
import * as vscode from 'vscode'
import { Messenger } from './messenger/messenger'
import { Database } from '../../../shared/db/chatDb/chatDb'
import { TabBarButtonClick, SaveChatMessage } from './model'
import { Conversation, messageToChatItem, Tab } from '../../../shared/db/chatDb/util'
import { DetailedListItemGroup, MynahIconsType } from '@aws/mynah-ui'
export class TabBarController {
private readonly messenger: Messenger
private chatHistoryDb = Database.getInstance()
private loadedChats: boolean = false
private searchTimeout: NodeJS.Timeout | undefined = undefined
private readonly DebounceTime = 300 // milliseconds
constructor(messenger: Messenger) {
this.messenger = messenger
}
async processActionClickMessage(msg: DetailedListActionClickMessage) {
if (msg.listType === 'history') {
if (msg.action.text === 'Delete') {
this.chatHistoryDb.deleteHistory(msg.action.id)
this.messenger.sendUpdateDetailedListMessage('history', { list: this.generateHistoryList() })
} else if (msg.action.text === 'Export') {
// If conversation is already open, export it
const openTabId = this.chatHistoryDb.getOpenTabId(msg.action.id)
if (openTabId) {
await this.exportChatButtonClicked({ tabID: openTabId, buttonId: 'export_chat' })
} // If conversation is not open, restore it before exporting
else {
const selectedTab = this.chatHistoryDb.getTab(msg.action.id)
this.restoreTab(selectedTab, true)
}
}
}
}
async processFilterChangeMessage(msg: DetailedListFilterChangeMessage) {
if (msg.listType === 'history') {
const searchFilter = msg.filterValues['search']
if (typeof searchFilter !== 'string') {
return
}
// Clear any pending search
if (this.searchTimeout) {
clearTimeout(this.searchTimeout)
}
// Set new timeout for this search
this.searchTimeout = setTimeout(() => {
const searchResults = this.chatHistoryDb.searchMessages(searchFilter)
this.messenger.sendUpdateDetailedListMessage('history', { list: searchResults })
}, this.DebounceTime)
}
}
// If selected is conversation is already open, select that tab. Else, open new tab with conversation.
processItemSelectMessage(msg: DetailedListItemSelectMessage) {
if (msg.listType === 'history') {
const historyID = msg.item.id
if (historyID) {
const openTabID = this.chatHistoryDb.getOpenTabId(historyID)
if (!openTabID) {
const selectedTab = this.chatHistoryDb.getTab(historyID)
this.restoreTab(selectedTab)
} else {
this.messenger.sendSelectTabMessage(openTabID, historyID)
}
this.messenger.sendCloseDetailedListMessage('history')
}
}
}
restoreTab(selectedTab?: Tab | null, exportTab?: boolean) {
if (selectedTab) {
this.messenger.sendRestoreTabMessage(
selectedTab.historyId,
selectedTab.tabType,
selectedTab.conversations.flatMap((conv: Conversation) =>
conv.messages.map((message) => messageToChatItem(message))
),
exportTab
)
}
}
loadChats() {
if (this.loadedChats) {
return
}
this.loadedChats = true
const openConversations = this.chatHistoryDb.getOpenTabs()
if (openConversations) {
for (const conversation of openConversations) {
if (conversation.conversations && conversation.conversations.length > 0) {
this.restoreTab(conversation)
}
}
}
}
async historyButtonClicked(message: TabBarButtonClick) {
this.messenger.sendOpenDetailedListMessage(message.tabID, 'history', {
header: { title: 'Chat history' },
filterOptions: [
{
type: 'textinput',
icon: 'search' as MynahIconsType,
id: 'search',
placeholder: 'Search...',
autoFocus: true,
},
],
list: this.generateHistoryList(),
})
}
generateHistoryList(): DetailedListItemGroup[] {
const historyItems = this.chatHistoryDb.getHistory()
return historyItems.length > 0 ? historyItems : [{ children: [{ description: 'No chat history found' }] }]
}
async processSaveChat(message: SaveChatMessage) {
try {
await fs.writeFile(message.uri, message.serializedChat)
} catch (error) {
void vscode.window.showErrorMessage('An error occurred while exporting your chat.')
}
}
async processTabBarButtonClick(message: TabBarButtonClick) {
switch (message.buttonId) {
case 'history_sheet':
await this.historyButtonClicked(message)
break
case 'export_chat':
await this.exportChatButtonClicked(message)
break
}
}
private async exportChatButtonClicked(message: TabBarButtonClick) {
const defaultFileName = `q-dev-chat-${new Date().toISOString().split('T')[0]}.md`
const workspaceFolders = vscode.workspace.workspaceFolders
let defaultUri
if (workspaceFolders && workspaceFolders.length > 0) {
// Use the first workspace folder as root
defaultUri = vscode.Uri.joinPath(workspaceFolders[0].uri, defaultFileName)
} else {
// Fallback if no workspace is open
defaultUri = vscode.Uri.file(defaultFileName)
}
const saveUri = await vscode.window.showSaveDialog({
filters: {
Markdown: ['md'],
HTML: ['html'],
},
defaultUri,
title: 'Export chat',
})
if (saveUri) {
// Determine format from file extension
const format = saveUri.fsPath.endsWith('.md') ? 'markdown' : 'html'
this.messenger.sendSerializeTabMessage(message.tabID, saveUri.fsPath, format)
}
}
}