Skip to content

Commit 12999ac

Browse files
authored
feat: Add Thinking LSP protocol types and generateTitle request (Thinking Part2) (#156)
Adds the protocol surface for streaming model-thinking content from the language server: - Thinking record + ThinkingTypeAdapter (tolerates the server's mixed wire shape for ext: string delta or array of fragments). - ChatProgressValue.thinking field (with equals/hashCode/toString updates). - thinking/generateTitle JSON request (GenerateThinkingTitleParams/Response, server interface, connection wrapper). - Register ThinkingTypeAdapter.Factory in CopilotLauncherBuilder.
1 parent 49f0123 commit 12999ac

8 files changed

Lines changed: 169 additions & 3 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel;
3333
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult;
3434
import com.microsoft.copilot.eclipse.core.lsp.protocol.DidShowInlineEditParams;
35+
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleParams;
36+
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleResponse;
3537
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolInformation;
3638
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsParams;
3739
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsResult;
@@ -195,6 +197,12 @@ public interface CopilotLanguageServer extends LanguageServer {
195197
@JsonRequest("git/commitGenerate")
196198
CompletableFuture<GenerateCommitMessageResult> generateCommitMessage(GenerateCommitMessageParams params);
197199

200+
/**
201+
* Generate a short title summarizing a thinking block.
202+
*/
203+
@JsonRequest("thinking/generateTitle")
204+
CompletableFuture<GenerateThinkingTitleResponse> generateThinkingTitle(GenerateThinkingTitleParams params);
205+
198206
/**
199207
* List BYOK models.
200208
*/

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult;
5151
import com.microsoft.copilot.eclipse.core.lsp.protocol.DidChangeCopilotWatchedFilesParams;
5252
import com.microsoft.copilot.eclipse.core.lsp.protocol.DidShowInlineEditParams;
53+
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleParams;
54+
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleResponse;
5355
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolInformation;
5456
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsParams;
5557
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsResult;
@@ -526,6 +528,19 @@ public CompletableFuture<GenerateCommitMessageResult> generateCommitMessage(Gene
526528
});
527529
}
528530

531+
/**
532+
* Generate a short title summarizing a thinking block.
533+
*/
534+
public CompletableFuture<GenerateThinkingTitleResponse> generateThinkingTitle(
535+
GenerateThinkingTitleParams params) {
536+
Function<LanguageServer, CompletableFuture<GenerateThinkingTitleResponse>> fn =
537+
server -> ((CopilotLanguageServer) server).generateThinkingTitle(params);
538+
return this.languageServerWrapper.execute(fn).exceptionally(ex -> {
539+
CopilotCore.LOGGER.error(ex);
540+
return null;
541+
});
542+
}
543+
529544
/**
530545
* List BYOK models.
531546
*/

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatReferenceTypeAdapter;
1010
import com.microsoft.copilot.eclipse.core.lsp.protocol.ProgressParamsAdapter;
11+
import com.microsoft.copilot.eclipse.core.lsp.protocol.ThinkingTypeAdapter;
1112

1213
/**
1314
* Builder for Copilot Language Server.
@@ -19,7 +20,8 @@ public class CopilotLauncherBuilder<T extends LanguageServer> extends Launcher.B
1920
*/
2021
public CopilotLauncherBuilder() {
2122
this.configureGson(gsonBuilder -> gsonBuilder.registerTypeAdapterFactory(new ProgressParamsAdapter.Factory())
22-
.registerTypeAdapterFactory(new ChatReferenceTypeAdapter.Factory()));
23+
.registerTypeAdapterFactory(new ChatReferenceTypeAdapter.Factory())
24+
.registerTypeAdapterFactory(new ThinkingTypeAdapter.Factory()));
2325
}
2426

2527
}

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class ChatProgressValue implements WorkDoneProgressNotification {
2525
private ChatReference[] references;
2626
private boolean hideText;
2727
private String[] notifications;
28+
private Thinking thinking;
2829
private ChatStep[] steps;
2930
private String cancellationReason;
3031
private ConversationError error;
@@ -80,6 +81,10 @@ public String[] getNotifications() {
8081
return notifications;
8182
}
8283

84+
public Thinking getThinking() {
85+
return thinking;
86+
}
87+
8388
public ChatStep[] getSteps() {
8489
return steps;
8590
}
@@ -140,6 +145,10 @@ public void setNotifications(String[] notifications) {
140145
this.notifications = notifications;
141146
}
142147

148+
public void setThinking(Thinking thinking) {
149+
this.thinking = thinking;
150+
}
151+
143152
public void setSteps(ChatStep[] steps) {
144153
this.steps = steps;
145154
}
@@ -177,7 +186,7 @@ public int hashCode() {
177186
result = prime * result + Arrays.hashCode(references);
178187
result = prime * result + Arrays.hashCode(steps);
179188
result = prime * result + Objects.hash(editAgentRounds, cancellationReason, contextSize, conversationId, error,
180-
hideText, kind, reply, title, turnId, parentTurnId, suggestedTitle);
189+
hideText, kind, reply, thinking, title, turnId, parentTurnId, suggestedTitle);
181190
return result;
182191
}
183192

@@ -199,7 +208,8 @@ public boolean equals(Object obj) {
199208
&& Objects.equals(conversationId, other.conversationId) && Objects.equals(error, other.error)
200209
&& hideText == other.hideText && kind == other.kind && Arrays.equals(notifications, other.notifications)
201210
&& Arrays.equals(references, other.references) && Objects.equals(reply, other.reply)
202-
&& Arrays.equals(steps, other.steps) && Objects.equals(title, other.title)
211+
&& Arrays.equals(steps, other.steps) && Objects.equals(thinking, other.thinking)
212+
&& Objects.equals(title, other.title)
203213
&& Objects.equals(turnId, other.turnId) && Objects.equals(parentTurnId, other.parentTurnId)
204214
&& Objects.equals(suggestedTitle, other.suggestedTitle);
205215
}
@@ -217,6 +227,7 @@ public String toString() {
217227
builder.append("references", Arrays.toString(references));
218228
builder.append("hideText", hideText);
219229
builder.append("notifications", Arrays.toString(notifications));
230+
builder.append("thinking", thinking);
220231
builder.append("steps", Arrays.toString(steps));
221232
builder.append("cancellationReason", cancellationReason);
222233
builder.append("error", error);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.core.lsp.protocol;
5+
6+
/**
7+
* Parameters for the {@code thinking/generateTitle} request.
8+
*
9+
* <p>Either {@code thinkingContent} or {@code extractedTitles} should be provided depending on
10+
* whether parsed section titles are available on the client side.
11+
*
12+
* @param thinkingContent the raw thinking content (used when no extracted titles are available)
13+
* @param extractedTitles previously extracted section titles, may be {@code null}
14+
*/
15+
public record GenerateThinkingTitleParams(String thinkingContent, String[] extractedTitles) {
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.core.lsp.protocol;
5+
6+
/**
7+
* Response for the {@code thinking/generateTitle} request.
8+
*
9+
* @param title the title returned by the language server
10+
*/
11+
public record GenerateThinkingTitleResponse(String title) {
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.core.lsp.protocol;
5+
6+
/**
7+
* Wire-level "thinking" payload streamed from the language server inside ChatProgressValue. Each report carries a
8+
* delta; callers accumulate the deltas across reports.
9+
*
10+
* @param id the (optional) identifier of the thinking block
11+
* @param text the delta text for this report; callers should treat blank text as "no content"
12+
* @param encrypted the (optional) encrypted form of the thinking content
13+
*/
14+
public record Thinking(String id, String text, String encrypted) {
15+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package com.microsoft.copilot.eclipse.core.lsp.protocol;
5+
6+
import java.io.IOException;
7+
8+
import com.google.gson.Gson;
9+
import com.google.gson.JsonArray;
10+
import com.google.gson.JsonElement;
11+
import com.google.gson.JsonNull;
12+
import com.google.gson.JsonObject;
13+
import com.google.gson.JsonPrimitive;
14+
import com.google.gson.TypeAdapter;
15+
import com.google.gson.TypeAdapterFactory;
16+
import com.google.gson.reflect.TypeToken;
17+
import com.google.gson.stream.JsonReader;
18+
import com.google.gson.stream.JsonWriter;
19+
20+
/**
21+
* TypeAdapter for {@link Thinking} that tolerates the server's mixed wire shape for
22+
* {@code text}: it may be a string delta (e.g. {@code "text":"The"}) or an array
23+
* (e.g. {@code "text":[]}) when the report carries only an encrypted payload, or a
24+
* multi-fragment array of deltas. The array form is concatenated into a single string
25+
* (or {@code null} when empty) so the rest of the UI treats the report as scalar.
26+
*/
27+
public class ThinkingTypeAdapter extends TypeAdapter<Thinking> {
28+
private final TypeAdapter<Thinking> delegate;
29+
private final TypeAdapter<JsonElement> elementAdapter;
30+
31+
/**
32+
* Construct a new adapter that delegates to the given default adapter after the
33+
* {@code text} field has been normalized.
34+
*
35+
* @param delegate the Gson-generated default adapter for {@link Thinking}
36+
* @param elementAdapter the adapter used to read the JSON tree
37+
*/
38+
public ThinkingTypeAdapter(TypeAdapter<Thinking> delegate, TypeAdapter<JsonElement> elementAdapter) {
39+
this.delegate = delegate;
40+
this.elementAdapter = elementAdapter;
41+
}
42+
43+
/** TypeAdapterFactory for {@link Thinking}. */
44+
public static final class Factory implements TypeAdapterFactory {
45+
@SuppressWarnings("unchecked")
46+
@Override
47+
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
48+
if (typeToken.getRawType() != Thinking.class) {
49+
return null;
50+
}
51+
TypeAdapter<Thinking> defaultAdapter = gson.getDelegateAdapter(this, TypeToken.get(Thinking.class));
52+
TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
53+
return (TypeAdapter<T>) new ThinkingTypeAdapter(defaultAdapter, elementAdapter);
54+
}
55+
}
56+
57+
@Override
58+
public Thinking read(JsonReader in) throws IOException {
59+
JsonElement element = elementAdapter.read(in);
60+
if (element != null && element.isJsonObject()) {
61+
JsonObject obj = element.getAsJsonObject();
62+
JsonElement text = obj.get("text");
63+
if (text != null && text.isJsonArray()) {
64+
obj.add("text", flattenTextArray(text.getAsJsonArray()));
65+
}
66+
}
67+
return delegate.fromJsonTree(element);
68+
}
69+
70+
private static JsonElement flattenTextArray(JsonArray arr) {
71+
StringBuilder sb = new StringBuilder();
72+
for (JsonElement e : arr) {
73+
if (!e.isJsonNull() && e.isJsonPrimitive()) {
74+
JsonPrimitive prim = e.getAsJsonPrimitive();
75+
if (prim.isString()) {
76+
sb.append(prim.getAsString());
77+
}
78+
}
79+
}
80+
return sb.length() == 0 ? JsonNull.INSTANCE : new JsonPrimitive(sb.toString());
81+
}
82+
83+
@Override
84+
public void write(JsonWriter out, Thinking value) throws IOException {
85+
delegate.write(out, value);
86+
}
87+
}

0 commit comments

Comments
 (0)