diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 4e8715bd5..895c0e7c7 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -2138,12 +2138,13 @@ public void OnSessionLifecycle(string type, string sessionId, JsonElement? metad client.DispatchLifecycleEvent(evt); } - public async ValueTask OnUserInputRequest(string sessionId, string question, IList? choices = null, bool? allowFreeform = null) + public async ValueTask OnUserInputRequest(string sessionId, string question, IList? choices = null, bool? allowFreeform = null, string? header = null) { var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); var request = new UserInputRequest { Question = question, + Header = header, Choices = choices, AllowFreeform = allowFreeform }; diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 7a2ad2951..aea62e96f 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -759,6 +759,12 @@ public sealed class UserInputRequest [JsonPropertyName("question")] public string Question { get; set; } = string.Empty; + /// + /// Optional short title summarizing the question, shown as the dialog header/title in some UIs. + /// + [JsonPropertyName("header")] + public string? Header { get; set; } + /// /// Optional choices for multiple choice questions. /// diff --git a/go/README.md b/go/README.md index 0ceaabb73..d654d4473 100644 --- a/go/README.md +++ b/go/README.md @@ -649,6 +649,7 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi Model: "gpt-5", OnUserInputRequest: func(request copilot.UserInputRequest, invocation copilot.UserInputInvocation) (copilot.UserInputResponse, error) { // request.Question - The question to ask + // request.Header - Optional short title summarizing the question (suitable as a dialog title) // request.Choices - Optional slice of choices for multiple choice // request.AllowFreeform - Whether freeform input is allowed (default: true) diff --git a/go/client.go b/go/client.go index cad460557..036a5e6a7 100644 --- a/go/client.go +++ b/go/client.go @@ -1955,6 +1955,7 @@ func (c *Client) handleUserInputRequest(req userInputRequest) (*userInputRespons response, err := session.handleUserInputRequest(UserInputRequest{ Question: req.Question, + Header: req.Header, Choices: req.Choices, AllowFreeform: req.AllowFreeform, }) diff --git a/go/types.go b/go/types.go index 7ffd454a3..a1410e708 100644 --- a/go/types.go +++ b/go/types.go @@ -312,6 +312,7 @@ type PermissionInvocation struct { // UserInputRequest represents a request for user input from the agent type UserInputRequest struct { Question string + Header *string Choices []string AllowFreeform *bool } @@ -1980,6 +1981,7 @@ type sessionEventRequest struct { type userInputRequest struct { SessionID string `json:"sessionId"` Question string `json:"question"` + Header *string `json:"header,omitempty"` Choices []string `json:"choices,omitempty"` AllowFreeform *bool `json:"allowFreeform,omitempty"` } diff --git a/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java b/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java index 391f270db..92d09addf 100644 --- a/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java +++ b/java/src/main/java/com/github/copilot/RpcHandlerDispatcher.java @@ -251,6 +251,7 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod String sessionId = params.get("sessionId").asText(); String question = params.get("question").asText(); LOG.fine("Processing userInput for session " + sessionId + ", question: " + question); + JsonNode headerNode = params.get("header"); JsonNode choicesNode = params.get("choices"); JsonNode allowFreeformNode = params.get("allowFreeform"); @@ -263,6 +264,9 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod } var request = new UserInputRequest().setQuestion(question); + if (headerNode != null && !headerNode.isNull()) { + request.setHeader(headerNode.asText()); + } if (choicesNode != null && choicesNode.isArray()) { var choices = new ArrayList(); for (JsonNode choice : choicesNode) { diff --git a/java/src/main/java/com/github/copilot/rpc/UserInputRequest.java b/java/src/main/java/com/github/copilot/rpc/UserInputRequest.java index 8e3551c5a..9dacc045b 100644 --- a/java/src/main/java/com/github/copilot/rpc/UserInputRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/UserInputRequest.java @@ -28,6 +28,9 @@ public class UserInputRequest { @JsonProperty("question") private String question; + @JsonProperty("header") + private String header; + @JsonProperty("choices") private List choices; @@ -55,6 +58,28 @@ public UserInputRequest setQuestion(String question) { return this; } + /** + * Gets the optional short title summarizing the question, suitable for display + * as the dialog header/title. + * + * @return the header text, or {@code null} if not specified + */ + public String getHeader() { + return header; + } + + /** + * Sets the optional short title summarizing the question. + * + * @param header + * the header text + * @return this instance for method chaining + */ + public UserInputRequest setHeader(String header) { + this.header = header; + return this; + } + /** * Gets the optional choices for multiple choice questions. * diff --git a/nodejs/README.md b/nodejs/README.md index 4219d3bc2..b83710b5b 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -888,6 +888,7 @@ const session = await client.createSession({ model: "gpt-5", onUserInputRequest: async (request, invocation) => { // request.question - The question to ask + // request.header - Optional short title summarizing the question (suitable as a dialog title) // request.choices - Optional array of choices for multiple choice // request.allowFreeform - Whether freeform input is allowed (default: true) diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 8dc35b8d7..f001cb770 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -2178,6 +2178,7 @@ export class CopilotClient { async (params: { sessionId: string; question: string; + header?: string; choices?: string[]; allowFreeform?: boolean; }): Promise<{ answer: string; wasFreeform: boolean }> => @@ -2309,6 +2310,7 @@ export class CopilotClient { private async handleUserInputRequest(params: { sessionId: string; question: string; + header?: string; choices?: string[]; allowFreeform?: boolean; }): Promise<{ answer: string; wasFreeform: boolean }> { @@ -2327,6 +2329,7 @@ export class CopilotClient { const result = await session._handleUserInputRequest({ question: params.question, + header: params.header, choices: params.choices, allowFreeform: params.allowFreeform, }); diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 75aa5159f..79aaf44e9 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -960,6 +960,11 @@ export interface UserInputRequest { */ question: string; + /** + * Optional short title summarizing the question, shown as the dialog header/title in some UIs + */ + header?: string; + /** * Optional choices for multiple choice questions */ diff --git a/python/README.md b/python/README.md index a916f98ec..5f00da652 100644 --- a/python/README.md +++ b/python/README.md @@ -660,6 +660,7 @@ Enable the agent to ask questions to the user using the `ask_user` tool by provi ```python async def handle_user_input(request, invocation): # request["question"] - The question to ask + # request.get("header") - Optional short title summarizing the question (suitable as a dialog title) # request.get("choices") - Optional list of choices for multiple choice # request.get("allowFreeform", True) - Whether freeform input is allowed diff --git a/python/copilot/session.py b/python/copilot/session.py index 32201870c..d49fa5b22 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -303,6 +303,7 @@ class UserInputRequest(TypedDict, total=False): """Request for user input from the agent (enables ask_user tool)""" question: str + header: str choices: list[str] allowFreeform: bool @@ -2088,12 +2089,16 @@ async def _handle_user_input_request(self, request: dict) -> UserInputResponse: try: handler_start = time.perf_counter() + user_input_request = UserInputRequest( + question=request.get("question", ""), + choices=request.get("choices") or [], + allowFreeform=request.get("allowFreeform", True), + ) + header = request.get("header") + if header is not None: + user_input_request["header"] = header result = handler( - UserInputRequest( - question=request.get("question", ""), - choices=request.get("choices") or [], - allowFreeform=request.get("allowFreeform", True), - ), + user_input_request, {"session_id": self.session_id}, ) if inspect.isawaitable(result): diff --git a/rust/README.md b/rust/README.md index 0b5bec1cd..0fe79b30e 100644 --- a/rust/README.md +++ b/rust/README.md @@ -481,6 +481,7 @@ impl UserInputHandler for MyUserInput { &self, _sid: SessionId, question: String, + _header: Option, _choices: Option>, _allow_freeform: Option, ) -> Option { diff --git a/rust/examples/chat.rs b/rust/examples/chat.rs index 6b361fdea..83f583ef8 100644 --- a/rust/examples/chat.rs +++ b/rust/examples/chat.rs @@ -26,6 +26,7 @@ impl UserInputHandler for StdinUserInputHandler { &self, _session_id: SessionId, question: String, + _header: Option, _choices: Option>, _allow_freeform: Option, ) -> Option { diff --git a/rust/src/handler.rs b/rust/src/handler.rs index dadd1706f..699d54aae 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -171,6 +171,7 @@ pub trait UserInputHandler: Send + Sync + 'static { &self, session_id: SessionId, question: String, + header: Option, choices: Option>, allow_freeform: Option, ) -> Option; diff --git a/rust/src/session.rs b/rust/src/session.rs index f387b8627..2ecc1daa1 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -2088,6 +2088,10 @@ async fn handle_request( return; }; let question = question.to_string(); + let header = params + .and_then(|p| p.get("header")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); let choices = params .and_then(|p| p.get("choices")) .and_then(|v| v.as_array()) @@ -2103,7 +2107,7 @@ async fn handle_request( let handler_start = Instant::now(); let response = if let Some(user_input_handler) = handlers.user_input.as_ref() { user_input_handler - .handle(sid.clone(), question, choices, allow_freeform) + .handle(sid.clone(), question, header, choices, allow_freeform) .await } else { None diff --git a/rust/tests/e2e/ask_user.rs b/rust/tests/e2e/ask_user.rs index 282af7d30..c3fac519f 100644 --- a/rust/tests/e2e/ask_user.rs +++ b/rust/tests/e2e/ask_user.rs @@ -170,6 +170,7 @@ impl UserInputHandler for RecordingUserInputHandler { &self, session_id: SessionId, question: String, + _header: Option, choices: Option>, allow_freeform: Option, ) -> Option { diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 244885697..10ecf22b8 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -1366,6 +1366,7 @@ async fn user_input_request_dispatches_to_handler() { &self, _session_id: SessionId, question: String, + _header: Option, _choices: Option>, _allow_freeform: Option, ) -> Option { @@ -1530,6 +1531,7 @@ async fn user_input_requested_notification_does_not_double_dispatch() { &self, _session_id: SessionId, _question: String, + _header: Option, _choices: Option>, _allow_freeform: Option, ) -> Option { @@ -1945,6 +1947,7 @@ async fn stop_event_loop_completes_in_flight_handler() { &self, _session_id: SessionId, _question: String, + _header: Option, _choices: Option>, _allow_freeform: Option, ) -> Option { @@ -2023,6 +2026,7 @@ async fn drop_session_does_not_abort_handler() { &self, _session_id: SessionId, _question: String, + _header: Option, _choices: Option>, _allow_freeform: Option, ) -> Option {