Skip to content

Commit 63c65c1

Browse files
committed
feat: render streaming thinking blocks in chat turns
Renders the model's thinking deltas in a collapsible banner above each assistant turn (Copilot and subagent), backed by the LSP plumbing added in the previous PR. - ThinkingBlock: collapsible streaming banner (spinner -> generated title / cancelled), markdown body parsed into titled sections. - ThinkingSection: gutter dot + bold title + markdown body for each parsed section. - ThinkingTurnWidget: extends BaseTurnWidget, accumulates Thinking deltas, seals on first reply/agent round, fires thinking/generateTitle, and finalizes on cancel. - BaseTurnWidget: onChatMessageCancelled hook (default no-op) so subclasses can react to cancel events. - CopilotTurnWidget / SubagentTurnWidget / SubagentMessageBlock: re-parented onto ThinkingTurnWidget. - ChatContentViewer: routes thinking deltas to the active turn, seals before reply/agent rounds, seals at WorkDoneProgressKind.end. - ChatView: don't drop progress reports that carry only thinking content. - Messages / messages.properties: NLS keys for the in-progress / cancelled / expand / collapse strings. - dark.css / light.css: shared secondary-text color rule for the new ThinkingBlock selector.
1 parent e7fbefd commit 63c65c1

13 files changed

Lines changed: 689 additions & 24 deletions

File tree

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,10 @@
115115
background-color: #2F3030;
116116
}
117117

118-
#chat-content-viewer > Composite > CopilotTurnWidget > AgentStatusLabel > .text-secondary {
119-
color: #A4A4A4;
120-
}
121-
122-
#chat-content-viewer > Composite > CopilotTurnWidget > AgentToolCancelLabel > Label.text-secondary {
123-
color: #A4A4A4;
124-
}
125-
118+
#chat-content-viewer > Composite > CopilotTurnWidget > AgentStatusLabel > .text-secondary,
119+
#chat-content-viewer > Composite > CopilotTurnWidget > ThinkingBlock .text-secondary,
120+
#chat-content-viewer > Composite > CopilotTurnWidget > .subagent-message-block ThinkingBlock .text-secondary,
121+
#chat-content-viewer > Composite > CopilotTurnWidget > AgentToolCancelLabel > Label.text-secondary,
126122
#chat-content-viewer > Composite > CopilotTurnWidget > Composite > Label.model-info-label {
127123
color: #A4A4A4;
128124
}

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,10 @@
115115
background-color: #F1F1F2;
116116
}
117117

118-
#chat-content-viewer > Composite > CopilotTurnWidget > AgentStatusLabel > .text-secondary {
119-
color: #808080;
120-
}
121-
122-
#chat-content-viewer > Composite > CopilotTurnWidget > AgentToolCancelLabel > Label.text-secondary {
123-
color: #808080;
124-
}
125-
118+
#chat-content-viewer > Composite > CopilotTurnWidget > AgentStatusLabel > .text-secondary,
119+
#chat-content-viewer > Composite > CopilotTurnWidget > ThinkingBlock .text-secondary,
120+
#chat-content-viewer > Composite > CopilotTurnWidget > .subagent-message-block ThinkingBlock .text-secondary,
121+
#chat-content-viewer > Composite > CopilotTurnWidget > AgentToolCancelLabel > Label.text-secondary,
126122
#chat-content-viewer > Composite > CopilotTurnWidget > Composite > Label.model-info-label {
127123
color: #808080;
128124
}

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,16 @@ protected BaseTurnWidget(Composite parent, int style, ChatServiceManager service
105105
IEventBroker eventBroker = PlatformUI.getWorkbench().getService(IEventBroker.class);
106106
this.cancelMsgEventHandler = event -> {
107107
cancelToolConfirmation();
108+
onChatMessageCancelled();
108109
};
109110
eventBroker.subscribe(CopilotEventConstants.TOPIC_CHAT_MESSAGE_CANCELLED, cancelMsgEventHandler);
110111
}
111112

113+
/** Hook invoked when the chat message cancel event is broadcast. Default no-op; subclasses may override. */
114+
protected void onChatMessageCancelled() {
115+
// no-op
116+
}
117+
112118
public String getTurnId() {
113119
return turnId;
114120
}
@@ -411,7 +417,8 @@ private void reset() {
411417
this.currentTextBlock = null;
412418
this.inCodeBlock = false;
413419

414-
// Don't reset subagent block state here - it's managed by tool call status
420+
// Subagent and thinking blocks have their own lifecycle (tool call status / ThinkingTurnWidget)
421+
// and are intentionally not reset here.
415422
}
416423

417424
/**

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ public void processTurnEvent(ChatProgressValue value) {
180180
ChatServiceManager chatServiceManager = CopilotUi.getPlugin().getChatServiceManager();
181181

182182
if (value.getKind() == WorkDoneProgressKind.report) {
183+
if (turnWidget instanceof ThinkingTurnWidget thinkingTurn) {
184+
thinkingTurn.appendThinking(value.getThinking());
185+
if (hasRenderableOutput(value)) {
186+
// Seal before appending the reply so the spinner stops and the title is fetched.
187+
thinkingTurn.sealThinking();
188+
}
189+
}
190+
183191
if (value.getAgentRounds() != null && !value.getAgentRounds().isEmpty()) {
184192
// Handle agent mode responses
185193
AgentRound agentRound = value.getAgentRounds().get(0);
@@ -200,6 +208,10 @@ public void processTurnEvent(ChatProgressValue value) {
200208
turnWidget.appendMessage(value.getReply());
201209
}
202210
} else if (value.getKind() == WorkDoneProgressKind.end) {
211+
// Seal any in-progress thinking block before the turn ends.
212+
if (turnWidget instanceof ThinkingTurnWidget thinkingTurn) {
213+
thinkingTurn.sealThinking();
214+
}
203215
turnWidget.notifyTurnEnd();
204216
}
205217
refreshScrollerLayout();
@@ -259,6 +271,29 @@ public void appendMessageToTheLatestTurn(String message) {
259271
}
260272
}
261273

274+
/**
275+
* Whether {@code value} carries reply text or an agent round with rendered content; thinking-only reports return
276+
* {@code false} so the banner keeps streaming.
277+
*/
278+
private static boolean hasRenderableOutput(ChatProgressValue value) {
279+
return StringUtils.isNotBlank(value.getReply()) || hasRenderableAgentRound(value);
280+
}
281+
282+
private static boolean hasRenderableAgentRound(ChatProgressValue value) {
283+
if (value.getAgentRounds() == null || value.getAgentRounds().isEmpty()) {
284+
return false;
285+
}
286+
for (AgentRound round : value.getAgentRounds()) {
287+
if (StringUtils.isNotBlank(round.getReply())) {
288+
return true;
289+
}
290+
if (round.getToolCalls() != null && !round.getToolCalls().isEmpty()) {
291+
return true;
292+
}
293+
}
294+
return false;
295+
}
296+
262297
/**
263298
* Process todo list from tool call result. Extracts todo list data from the tool-specific data
264299
* and updates the TodoListService.

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,8 @@ public void onChatProgress(ChatProgressValue value) {
823823
}
824824
}
825825
if ((value.getAgentRounds() == null || value.getAgentRounds().isEmpty())
826-
&& (value.getReply() == null || value.getReply().isEmpty())) {
826+
&& (value.getReply() == null || value.getReply().isEmpty())
827+
&& (value.getThinking() == null || StringUtils.isBlank(value.getThinking().text()))) {
827828
return;
828829
}
829830
if (this.chatContentViewer != null) {

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/CopilotTurnWidget.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
/**
2424
* A custom widget that displays a turn for the copilot.
2525
*/
26-
public class CopilotTurnWidget extends BaseTurnWidget {
26+
public class CopilotTurnWidget extends ThinkingTurnWidget {
2727
/**
2828
* Create the widget.
2929
*/
3030
public CopilotTurnWidget(Composite parent, int style, ChatServiceManager serviceManager, String turnId) {
31-
super(parent, style, serviceManager, turnId, true, null);
31+
super(parent, style, serviceManager, turnId, null);
3232
setData("org.eclipse.swtbot.widget.key", "copilot-turn");
3333
}
3434

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/Messages.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public final class Messages extends NLS {
3737
public static String todoList_clearButtonDisabled;
3838
public static String todoList_expandTooltip;
3939
public static String todoList_collapseTooltip;
40+
public static String thinking_inProgressTitle;
41+
public static String thinking_cancelledTitle;
42+
public static String thinking_expandTooltip;
43+
public static String thinking_collapseTooltip;
4044

4145
static {
4246
// initialize resource bundle

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SubagentMessageBlock.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class SubagentMessageBlock extends Composite {
2424
private AgentToolCall toolCall;
2525

2626
// Track the current content widget for message processing
27-
private BaseTurnWidget currentSubagentTurnWidget;
27+
private ThinkingTurnWidget currentSubagentTurnWidget;
2828

2929
/**
3030
* Create the subagent message block.
@@ -102,7 +102,7 @@ public void notifyTurnEnd() {
102102
*
103103
* @return the subagent turn widget
104104
*/
105-
public BaseTurnWidget getSubagentTurnWidget() {
105+
public ThinkingTurnWidget getSubagentTurnWidget() {
106106
return currentSubagentTurnWidget;
107107
}
108108
}

com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SubagentTurnWidget.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919
* A turn widget for displaying subagent messages within a SubagentMessageBlock.
2020
* This widget doesn't show an avatar or role name, only the message content.
2121
*/
22-
public class SubagentTurnWidget extends BaseTurnWidget {
22+
public class SubagentTurnWidget extends ThinkingTurnWidget {
2323

2424
/**
2525
* Create the widget.
2626
*/
2727
public SubagentTurnWidget(Composite parent, int style, ChatServiceManager serviceManager, String turnId,
2828
AgentToolCall toolCall) {
29-
super(parent, style, serviceManager, turnId + "_subagent", true,
29+
super(parent, style, serviceManager, turnId + "_subagent",
3030
getToolCallRoleName(toolCall));
3131
}
3232

0 commit comments

Comments
 (0)