Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel;
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult;
import com.microsoft.copilot.eclipse.core.lsp.protocol.DidShowInlineEditParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolInformation;
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsResult;
Expand Down Expand Up @@ -195,6 +197,12 @@ public interface CopilotLanguageServer extends LanguageServer {
@JsonRequest("git/commitGenerate")
CompletableFuture<GenerateCommitMessageResult> generateCommitMessage(GenerateCommitMessageParams params);

/**
* Generate a short title summarizing a thinking block.
*/
@JsonRequest("thinking/generateTitle")
CompletableFuture<GenerateThinkingTitleResponse> generateThinkingTitle(GenerateThinkingTitleParams params);

/**
* List BYOK models.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult;
import com.microsoft.copilot.eclipse.core.lsp.protocol.DidChangeCopilotWatchedFilesParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.DidShowInlineEditParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolInformation;
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.NextEditSuggestionsResult;
Expand Down Expand Up @@ -526,6 +528,19 @@ public CompletableFuture<GenerateCommitMessageResult> generateCommitMessage(Gene
});
}

/**
* Generate a short title summarizing a thinking block.
*/
public CompletableFuture<GenerateThinkingTitleResponse> generateThinkingTitle(
GenerateThinkingTitleParams params) {
Function<LanguageServer, CompletableFuture<GenerateThinkingTitleResponse>> fn =
server -> ((CopilotLanguageServer) server).generateThinkingTitle(params);
return this.languageServerWrapper.execute(fn).exceptionally(ex -> {
CopilotCore.LOGGER.error(ex);
return null;
});
}

/**
* List BYOK models.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatReferenceTypeAdapter;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ProgressParamsAdapter;
import com.microsoft.copilot.eclipse.core.lsp.protocol.ThinkingTypeAdapter;

/**
* Builder for Copilot Language Server.
Expand All @@ -19,7 +20,8 @@ public class CopilotLauncherBuilder<T extends LanguageServer> extends Launcher.B
*/
public CopilotLauncherBuilder() {
this.configureGson(gsonBuilder -> gsonBuilder.registerTypeAdapterFactory(new ProgressParamsAdapter.Factory())
.registerTypeAdapterFactory(new ChatReferenceTypeAdapter.Factory()));
.registerTypeAdapterFactory(new ChatReferenceTypeAdapter.Factory())
.registerTypeAdapterFactory(new ThinkingTypeAdapter.Factory()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class ChatProgressValue implements WorkDoneProgressNotification {
private ChatReference[] references;
private boolean hideText;
private String[] notifications;
private Thinking thinking;
private ChatStep[] steps;
private String cancellationReason;
private ConversationError error;
Expand Down Expand Up @@ -80,6 +81,10 @@ public String[] getNotifications() {
return notifications;
}

public Thinking getThinking() {
return thinking;
}

public ChatStep[] getSteps() {
return steps;
}
Expand Down Expand Up @@ -140,6 +145,10 @@ public void setNotifications(String[] notifications) {
this.notifications = notifications;
}

public void setThinking(Thinking thinking) {
this.thinking = thinking;
}

public void setSteps(ChatStep[] steps) {
this.steps = steps;
}
Expand Down Expand Up @@ -177,7 +186,7 @@ public int hashCode() {
result = prime * result + Arrays.hashCode(references);
result = prime * result + Arrays.hashCode(steps);
result = prime * result + Objects.hash(editAgentRounds, cancellationReason, contextSize, conversationId, error,
hideText, kind, reply, title, turnId, parentTurnId, suggestedTitle);
hideText, kind, reply, thinking, title, turnId, parentTurnId, suggestedTitle);
return result;
}

Expand All @@ -199,7 +208,8 @@ public boolean equals(Object obj) {
&& Objects.equals(conversationId, other.conversationId) && Objects.equals(error, other.error)
&& hideText == other.hideText && kind == other.kind && Arrays.equals(notifications, other.notifications)
&& Arrays.equals(references, other.references) && Objects.equals(reply, other.reply)
&& Arrays.equals(steps, other.steps) && Objects.equals(title, other.title)
&& Arrays.equals(steps, other.steps) && Objects.equals(thinking, other.thinking)
&& Objects.equals(title, other.title)
&& Objects.equals(turnId, other.turnId) && Objects.equals(parentTurnId, other.parentTurnId)
&& Objects.equals(suggestedTitle, other.suggestedTitle);
}
Expand All @@ -217,6 +227,7 @@ public String toString() {
builder.append("references", Arrays.toString(references));
builder.append("hideText", hideText);
builder.append("notifications", Arrays.toString(notifications));
builder.append("thinking", thinking);
builder.append("steps", Arrays.toString(steps));
builder.append("cancellationReason", cancellationReason);
builder.append("error", error);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol;

/**
* Parameters for the {@code thinking/generateTitle} request.
*
* <p>Either {@code thinkingContent} or {@code extractedTitles} should be provided depending on
* whether parsed section titles are available on the client side.
*
* @param thinkingContent the raw thinking content (used when no extracted titles are available)
* @param extractedTitles previously extracted section titles, may be {@code null}
*/
public record GenerateThinkingTitleParams(String thinkingContent, String[] extractedTitles) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol;

/**
* Response for the {@code thinking/generateTitle} request.
*
* @param title the title returned by the language server
*/
public record GenerateThinkingTitleResponse(String title) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol;

/**
* Wire-level "thinking" payload streamed from the language server inside ChatProgressValue. Each report carries a
* delta; callers accumulate the deltas across reports.
*
* @param id the (optional) identifier of the thinking block
* @param text the delta text for this report; callers should treat blank text as "no content"
* @param encrypted the (optional) encrypted form of the thinking content
*/
public record Thinking(String id, String text, String encrypted) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol;

import java.io.IOException;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

/**
* TypeAdapter for {@link Thinking} that tolerates the server's mixed wire shape for
* {@code text}: it may be a string delta (e.g. {@code "text":"The"}) or an array
* (e.g. {@code "text":[]}) when the report carries only an encrypted payload, or a
* multi-fragment array of deltas. The array form is concatenated into a single string
* (or {@code null} when empty) so the rest of the UI treats the report as scalar.
*/
public class ThinkingTypeAdapter extends TypeAdapter<Thinking> {
private final TypeAdapter<Thinking> delegate;
private final TypeAdapter<JsonElement> elementAdapter;

/**
* Construct a new adapter that delegates to the given default adapter after the
* {@code text} field has been normalized.
*
* @param delegate the Gson-generated default adapter for {@link Thinking}
* @param elementAdapter the adapter used to read the JSON tree
*/
public ThinkingTypeAdapter(TypeAdapter<Thinking> delegate, TypeAdapter<JsonElement> elementAdapter) {
this.delegate = delegate;
this.elementAdapter = elementAdapter;
}

/** TypeAdapterFactory for {@link Thinking}. */
public static final class Factory implements TypeAdapterFactory {
@SuppressWarnings("unchecked")
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (typeToken.getRawType() != Thinking.class) {
return null;
}
TypeAdapter<Thinking> defaultAdapter = gson.getDelegateAdapter(this, TypeToken.get(Thinking.class));
TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return (TypeAdapter<T>) new ThinkingTypeAdapter(defaultAdapter, elementAdapter);
}
}

@Override
public Thinking read(JsonReader in) throws IOException {
JsonElement element = elementAdapter.read(in);
if (element != null && element.isJsonObject()) {
JsonObject obj = element.getAsJsonObject();
JsonElement text = obj.get("text");
if (text != null && text.isJsonArray()) {
obj.add("text", flattenTextArray(text.getAsJsonArray()));
}
}
return delegate.fromJsonTree(element);
}

private static JsonElement flattenTextArray(JsonArray arr) {
StringBuilder sb = new StringBuilder();
for (JsonElement e : arr) {
if (!e.isJsonNull() && e.isJsonPrimitive()) {
JsonPrimitive prim = e.getAsJsonPrimitive();
if (prim.isString()) {
sb.append(prim.getAsString());
}
}
}
return sb.length() == 0 ? JsonNull.INSTANCE : new JsonPrimitive(sb.toString());
}

@Override
public void write(JsonWriter out, Thinking value) throws IOException {
delegate.write(out, value);
}
}
Loading