Skip to content

Commit 6fda30e

Browse files
authored
feat - Chat History (Part3) - Added UI to persist chat history. (#1148)
1 parent f8ca891 commit 6fda30e

32 files changed

Lines changed: 1177 additions & 79 deletions

File tree

com.microsoft.copilot.eclipse.core/META-INF/MANIFEST.MF

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Export-Package: com.microsoft.copilot.eclipse.core,
1515
com.microsoft.copilot.eclipse.core.lsp.protocol,
1616
com.microsoft.copilot.eclipse.core.lsp.protocol.git,
1717
com.microsoft.copilot.eclipse.core.lsp.protocol.quota,
18+
com.microsoft.copilot.eclipse.core.persistence;x-friends:="com.microsoft.copilot.eclipse.ui,com.microsoft.copilot.eclipse.core.test,com.microsoft.copilot.eclipse.core.utils",
1819
com.microsoft.copilot.eclipse.core.utils
1920
Bundle-Activator: com.microsoft.copilot.eclipse.core.CopilotCore
2021
Bundle-RequiredExecutionEnvironment: JavaSE-17

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,34 @@ public class CopilotEventConstants {
4949
* Event when MCP runtime log is received.
5050
*/
5151
public static final String TOPIC_CHAT_MCP_RUNTIME_LOG = TOPIC_CHAT + "MCP_RUNTIME_LOG";
52-
52+
5353
/**
5454
* Event when the chat feature flag are updated.
5555
*/
5656
public static final String TOPIC_CHAT_DID_CHANGE_FEATURE_FLAGS = TOPIC_CHAT + "DID_CHANGE_FEATURE_FLAGS";
57-
57+
5858
/**
5959
* Event when the chat mode is changed.
6060
*/
6161
public static final String TOPIC_CHAT_MODE_CHANGED = TOPIC_CHAT + "MODE_CHANGED";
62+
63+
/**
64+
* Event when the chat history visibility is toggled to hide chat history.
65+
*/
66+
public static final String TOPIC_CHAT_HIDE_CHAT_HISTORY = TOPIC_CHAT + "HIDE_CHAT_HISTORY";
67+
68+
/**
69+
* Event when the chat history visibility is toggled to show chat history.
70+
*/
71+
public static final String TOPIC_CHAT_SHOW_CHAT_HISTORY = TOPIC_CHAT + "SHOW_CHAT_HISTORY";
72+
73+
/**
74+
* Event when the back button is clicked in chat history view.
75+
*/
76+
public static final String TOPIC_CHAT_HISTORY_BACK_CLICKED = TOPIC_CHAT + "BACK_TO_CHAT_CLICKED";
77+
78+
/**
79+
* Event when a conversation is selected in chat history view.
80+
*/
81+
public static final String TOPIC_CHAT_HISTORY_CONVERSATION_SELECTED = TOPIC_CHAT + "HISTORY_CONVERSATION_SELECTED";
6282
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInConfirmParams;
4343
import com.microsoft.copilot.eclipse.core.lsp.protocol.SignInInitiateResult;
4444
import com.microsoft.copilot.eclipse.core.lsp.protocol.TelemetryExceptionParams;
45+
import com.microsoft.copilot.eclipse.core.lsp.protocol.Turn;
4546
import com.microsoft.copilot.eclipse.core.lsp.protocol.UpdateMcpToolsStatusParams;
4647
import com.microsoft.copilot.eclipse.core.lsp.protocol.git.GenerateCommitMessageParams;
4748
import com.microsoft.copilot.eclipse.core.lsp.protocol.git.GenerateCommitMessageResult;
@@ -221,7 +222,7 @@ public CompletableFuture<Object> sendExceptionTelemetry(Throwable ex) {
221222
* Create a conversation with the given parameters.
222223
*/
223224
public CompletableFuture<ChatCreateResult> createConversation(String workDoneToken, String message,
224-
List<IResource> files, IFile currentFile, CopilotModel activeModel, String chatModeName) {
225+
List<IResource> files, IFile currentFile, List<Turn> turns, CopilotModel activeModel, String chatModeName) {
225226
boolean supportVision = activeModel.getCapabilities().supports().vision();
226227
Either<String, List<ChatCompletionContentPart>> messageWithImages = ChatMessageUtils
227228
.createMessageWithImages(message, FileUtils.filterFilesFrom(files), supportVision);
@@ -232,6 +233,11 @@ public CompletableFuture<ChatCreateResult> createConversation(String workDoneTok
232233
param.setReferences(FileUtils.convertToChatReferences(files));
233234
param.setModel(getModelName(activeModel));
234235
param.setChatMode(chatModeName);
236+
237+
// Set historical turns if provided.
238+
if (turns != null && turns.size() > 0) {
239+
param.setTurns(turns);
240+
}
235241
// TODO: remove needToolCallConfirmation when CLS fully supports it across all IDEs.
236242
param.setNeedToolCallConfirmation(true);
237243
if (currentFile != null) {

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ConversationCreateParams.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
public class ConversationCreateParams {
1717
private String workDoneToken;
18-
private Turn[] turns;
18+
private List<Turn> turns;
1919
private ConversationCapabilities capabilities;
2020
private boolean computeSuggestions;
2121
private TextDocumentIdentifier textDocument;
@@ -37,7 +37,7 @@ public class ConversationCreateParams {
3737
*/
3838
public ConversationCreateParams(Either<String, List<ChatCompletionContentPart>> prompt, String workDoneToken) {
3939
this.workDoneToken = workDoneToken;
40-
this.turns = new Turn[] { new Turn(prompt, null, null) };
40+
this.turns = new ArrayList<>(List.of(new Turn(prompt, null, null)));
4141
this.capabilities = new ConversationCapabilities();
4242
this.capabilities.setSkills(List.of(ConversationCapabilities.CURRENT_EDITOR_SKILL));
4343
this.computeSuggestions = true;
@@ -54,11 +54,11 @@ public void setWorkDoneToken(String workDoneToken) {
5454
this.workDoneToken = workDoneToken;
5555
}
5656

57-
public Turn[] getTurns() {
57+
public List<Turn> getTurns() {
5858
return turns;
5959
}
6060

61-
public void setTurns(Turn[] turns) {
61+
public void setTurns(List<Turn> turns) {
6262
this.turns = turns;
6363
}
6464

@@ -163,7 +163,7 @@ public int hashCode() {
163163
final int prime = 31;
164164
int result = 1;
165165
result = prime * result + Arrays.hashCode(ignoredSkills);
166-
result = prime * result + Arrays.hashCode(turns);
166+
result = prime * result + Objects.hashCode(turns);
167167
result = prime * result + Objects.hash(capabilities, chatMode, computeSuggestions, model, needToolCallConfirmation,
168168
references, source, textDocument, userLanguage, workDoneToken, workspaceFolder, workspaceFolders);
169169
return result;
@@ -185,7 +185,7 @@ public boolean equals(Object obj) {
185185
&& computeSuggestions == other.computeSuggestions && Arrays.equals(ignoredSkills, other.ignoredSkills)
186186
&& Objects.equals(model, other.model) && needToolCallConfirmation == other.needToolCallConfirmation
187187
&& Objects.equals(references, other.references) && Objects.equals(source, other.source)
188-
&& Objects.equals(textDocument, other.textDocument) && Arrays.equals(turns, other.turns)
188+
&& Objects.equals(textDocument, other.textDocument) && Objects.equals(turns, other.turns)
189189
&& Objects.equals(userLanguage, other.userLanguage) && Objects.equals(workDoneToken, other.workDoneToken)
190190
&& Objects.equals(workspaceFolder, other.workspaceFolder)
191191
&& Objects.equals(workspaceFolders, other.workspaceFolders);
@@ -195,7 +195,7 @@ public boolean equals(Object obj) {
195195
public String toString() {
196196
ToStringBuilder builder = new ToStringBuilder(this);
197197
builder.add("workDoneToken", workDoneToken);
198-
builder.add("turns", Arrays.toString(turns));
198+
builder.add("turns", turns);
199199
builder.add("capabilities", capabilities);
200200
builder.add("computeSuggestions", computeSuggestions);
201201
builder.add("textDocument", textDocument);

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/persistence/ConversationDataFactory.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,10 @@ public List<Turn> convertToTurns(List<AbstractTurnData> turnDataList) {
201201
if (turnDataList == null || turnDataList.isEmpty()) {
202202
return new ArrayList<>();
203203
}
204-
List<Turn> result = new ArrayList<>(turnDataList.size());
205-
for (AbstractTurnData turnData : turnDataList) {
204+
// Defensive copy to avoid ConcurrentModificationException if another thread mutates the list while iterating.
205+
List<AbstractTurnData> snapshot = new ArrayList<>(turnDataList);
206+
List<Turn> result = new ArrayList<>(snapshot.size());
207+
for (AbstractTurnData turnData : snapshot) {
206208
if (turnData == null) {
207209
continue;
208210
}
@@ -380,4 +382,52 @@ private void ensureReplyInitialized(ReplyData reply) {
380382
reply.setSteps(new ArrayList<>());
381383
}
382384
}
385+
386+
/**
387+
* Converts persisted ToolCallData to AgentToolCall for use with UI components. This is the reverse operation of
388+
* convertAgentToolCallToToolCallData.
389+
*
390+
* @param toolCallData the persisted tool call data
391+
* @return AgentToolCall instance for use with UI components
392+
*/
393+
public AgentToolCall convertToolCallDataToAgentToolCall(ToolCallData toolCallData) {
394+
if (toolCallData == null) {
395+
return null;
396+
}
397+
398+
return new AgentToolCall() {
399+
@Override
400+
public String getId() {
401+
return toolCallData.getId();
402+
}
403+
404+
@Override
405+
public String getName() {
406+
return toolCallData.getName();
407+
}
408+
409+
@Override
410+
public String getProgressMessage() {
411+
return toolCallData.getProgressMessage();
412+
}
413+
414+
@Override
415+
public String getStatus() {
416+
return toolCallData.getStatus();
417+
}
418+
419+
@Override
420+
public String getError() {
421+
// Extract error from result if present
422+
if (toolCallData.getResult() != null && !toolCallData.getResult().isEmpty()) {
423+
for (Map<String, Object> result : toolCallData.getResult()) {
424+
if (result.containsKey("error")) {
425+
return result.get("error").toString();
426+
}
427+
}
428+
}
429+
return null;
430+
}
431+
};
432+
}
383433
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/persistence/ConversationPersistenceManager.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ private ConversationData getOrCreateNewConversationById(String conversationId) t
257257
}
258258

259259
ConversationData newConversation = dataFactory.createConversationData(conversationId);
260+
261+
// must persist conversation and index here to make sure conversation title and last message date is up to date.
260262
persistAndCacheConversation(newConversation);
261263
return newConversation;
262264
}
@@ -265,4 +267,13 @@ private void persistAndCacheConversation(ConversationData conversation) throws I
265267
persistenceService.saveConversation(conversation);
266268
conversationCache.put(conversation.getConversationId(), conversation);
267269
}
268-
}
270+
271+
/**
272+
* Gets the data factory for data transformation operations.
273+
*
274+
* @return the ConversationDataFactory instance
275+
*/
276+
public ConversationDataFactory getDataFactory() {
277+
return dataFactory;
278+
}
279+
}

com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/persistence/ConversationPersistenceService.java

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public ConversationPersistenceService(AuthStatusManager authStatusManager) {
8282

8383
/**
8484
* Saves the conversation data to conversation JSON file and updates the index if it's a new conversation or the
85-
* conversation title should be updated.
85+
* conversation title / lastMessageDate should be updated.
8686
*/
8787
public void saveConversation(ConversationData conversationData) throws IOException {
8888
String username = authStatusManager.getUserName();
@@ -91,38 +91,8 @@ public void saveConversation(ConversationData conversationData) throws IOExcepti
9191
return;
9292
}
9393

94-
boolean shouldUpdateIndexConversationId = saveConversationJson(conversationData, username);
95-
boolean shouldUpdateIndexTitle = StringUtils.isNotBlank(conversationData.getTitle())
96-
&& isIndexTitleMissing(conversationData.getConversationId(), username);
97-
98-
if (shouldUpdateIndexConversationId || shouldUpdateIndexTitle) {
99-
updateConversationIndex(conversationData, username);
100-
}
101-
}
102-
103-
/**
104-
* Checks if the conversation title is missing or empty in the XML index.
105-
*/
106-
private boolean isIndexTitleMissing(String conversationId, String username) {
107-
try {
108-
Document doc = loadOrCreateIndexDocument(username);
109-
Element userElement = getUserElement(doc, username, false);
110-
111-
if (userElement != null) {
112-
Element conversationsElement = findChildElement(userElement, ELEMENT_CONVERSATIONS);
113-
if (conversationsElement != null) {
114-
Element conversationElement = findConversationElement(conversationsElement, conversationId);
115-
if (conversationElement != null) {
116-
String existingTitle = conversationElement.getAttribute(ATTR_TITLE);
117-
return StringUtils.isBlank(existingTitle);
118-
}
119-
}
120-
}
121-
return true;
122-
} catch (ParserConfigurationException | SAXException | IOException e) {
123-
CopilotCore.LOGGER.error("Failed to check index title for conversation: " + conversationId, e);
124-
return true;
125-
}
94+
saveConversationJson(conversationData, username);
95+
updateConversationIndex(conversationData, username);
12696
}
12797

12898
/** Gets the plugin state location path for the current workspace. */

com.microsoft.copilot.eclipse.ui.terminal.tm/src/com/microsoft/copilot/eclipse/ui/terminal/tm/RunInTerminalTool.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,4 @@ private IWorkbenchPage getActivePage() {
329329
public void setTerminalIconDescriptor(ImageDescriptor iconDescriptor) {
330330
this.terminalIconDescriptor = iconDescriptor;
331331
}
332-
}
332+
}

com.microsoft.copilot.eclipse.ui.terminal/src/com/microsoft/copilot/eclipse/ui/terminal/RunInTerminalTool.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,4 @@ private IWorkbenchPage getActivePage() {
331331
public void setTerminalIconDescriptor(ImageDescriptor iconDescriptor) {
332332
this.terminalIconDescriptor = iconDescriptor;
333333
}
334-
}
334+
}

com.microsoft.copilot.eclipse.ui/css/dark.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
#chat-container > BeforeLoginWelcomeViewer,
4646
#chat-container > BeforeLoginWelcomeViewer *,
4747
#chat-content-wrapper,
48+
#chat-content-wrapper > ChatHistoryViewer,
49+
#chat-content-wrapper > ChatHistoryViewer *,
4850
#chat-content-wrapper > AfterLoginWelcomeViewer,
4951
#chat-content-wrapper > AfterLoginWelcomeViewer *,
5052
#chat-content-wrapper > AgentModeViewer,
@@ -96,3 +98,27 @@
9698
color: #DEE1E5;
9799
background-color: #26282B;
98100
}
101+
102+
#chat-history-viewer Composite.chat-history-item,
103+
#chat-history-viewer Composite.chat-history-item * {
104+
color: #D3D2D2; /* default/hover exit foreground */
105+
background-color: #1E1F22;/* default/hover exit background */
106+
}
107+
108+
#chat-history-viewer Composite.chat-history-item-current,
109+
#chat-history-viewer Composite.chat-history-item-current * {
110+
color: #D3D2D2; /* current item foreground */
111+
background-color: #274458;/* current item background */
112+
}
113+
114+
/* Hover state should override default/current */
115+
#chat-history-viewer Composite.chat-history-item-hover,
116+
#chat-history-viewer Composite.chat-history-item-hover * {
117+
color: #D3D2D2; /* hover foreground */
118+
background-color: #393B40;/* hover background */
119+
}
120+
121+
#chat-history-viewer Label.chat-history-item-current-label,
122+
#chat-history-viewer Label.chat-history-item-date-label {
123+
color: #A4A4A4;
124+
}

0 commit comments

Comments
 (0)