Skip to content
Closed
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
3 changes: 2 additions & 1 deletion dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2138,12 +2138,13 @@ public void OnSessionLifecycle(string type, string sessionId, JsonElement? metad
client.DispatchLifecycleEvent(evt);
}

public async ValueTask<UserInputRequestResponse> OnUserInputRequest(string sessionId, string question, IList<string>? choices = null, bool? allowFreeform = null)
public async ValueTask<UserInputRequestResponse> OnUserInputRequest(string sessionId, string question, IList<string>? 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
};
Expand Down
6 changes: 6 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,12 @@ public sealed class UserInputRequest
[JsonPropertyName("question")]
public string Question { get; set; } = string.Empty;

/// <summary>
/// Optional short title summarizing the question, shown as the dialog header/title in some UIs.
/// </summary>
[JsonPropertyName("header")]
public string? Header { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .NET code is updated here, but dotnet/README.md line 807 still documents only question, choices, and allowFreeform in the OnUserInputRequest example. All other SDK READMEs (Node, Python, Go, Rust) were updated to mention header — .NET is the odd one out.

Suggested addition at dotnet/README.md line 808 (after // request.Question - ...):

        // request.Header - Optional short title summarizing the question (suitable as a dialog title)


/// <summary>
/// Optional choices for multiple choice questions.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
2 changes: 2 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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"`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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<String>();
for (JsonNode choice : choicesNode) {
Expand Down
25 changes: 25 additions & 0 deletions java/src/main/java/com/github/copilot/rpc/UserInputRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public class UserInputRequest {
@JsonProperty("question")
private String question;

@JsonProperty("header")
private String header;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Java code correctly gains header, but the Java README (java/README.md) has no UserInputRequest / ask_user documentation section at all — unlike every other SDK README. This pre-existing gap is now more visible since the other READMEs all show the new field.

Consider adding a "User Input Requests" section to java/README.md (modelled after the Node/Python/Go/.NET equivalents) that documents the onUserInputRequest handler, including the new header field. For example:

var session = client.createSession(new SessionConfig()
    .setModel("gpt-5")
    .setOnUserInputRequest((request, invocation) -> {
        // request.getQuestion()      - The question to ask
        // request.getHeader()        - Optional short title suitable as a dialog title
        // request.getChoices()       - Optional list of choices for multiple choice
        // request.isAllowFreeform()  - Whether freeform input is allowed (default: true)
        return CompletableFuture.completedFuture(
            new UserInputResponse().setAnswer("User's answer here").setWasFreeform(true)
        );
    })).get();


@JsonProperty("choices")
private List<String> choices;

Expand Down Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,7 @@ export class CopilotClient {
async (params: {
sessionId: string;
question: string;
header?: string;
choices?: string[];
allowFreeform?: boolean;
}): Promise<{ answer: string; wasFreeform: boolean }> =>
Expand Down Expand Up @@ -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 }> {
Expand All @@ -2327,6 +2329,7 @@ export class CopilotClient {

const result = await session._handleUserInputRequest({
question: params.question,
header: params.header,
choices: params.choices,
allowFreeform: params.allowFreeform,
});
Expand Down
5 changes: 5 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 10 additions & 5 deletions python/copilot/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ impl UserInputHandler for MyUserInput {
&self,
_sid: SessionId,
question: String,
_header: Option<String>,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Option<UserInputResponse> {
Expand Down
1 change: 1 addition & 0 deletions rust/examples/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ impl UserInputHandler for StdinUserInputHandler {
&self,
_session_id: SessionId,
question: String,
_header: Option<String>,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Option<UserInputResponse> {
Expand Down
1 change: 1 addition & 0 deletions rust/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ pub trait UserInputHandler: Send + Sync + 'static {
&self,
session_id: SessionId,
question: String,
header: Option<String>,
choices: Option<Vec<String>>,
allow_freeform: Option<bool>,
) -> Option<UserInputResponse>;
Expand Down
6 changes: 5 additions & 1 deletion rust/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions rust/tests/e2e/ask_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ impl UserInputHandler for RecordingUserInputHandler {
&self,
session_id: SessionId,
question: String,
_header: Option<String>,
choices: Option<Vec<String>>,
allow_freeform: Option<bool>,
) -> Option<UserInputResponse> {
Expand Down
4 changes: 4 additions & 0 deletions rust/tests/session_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,7 @@ async fn user_input_request_dispatches_to_handler() {
&self,
_session_id: SessionId,
question: String,
_header: Option<String>,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Option<UserInputResponse> {
Expand Down Expand Up @@ -1530,6 +1531,7 @@ async fn user_input_requested_notification_does_not_double_dispatch() {
&self,
_session_id: SessionId,
_question: String,
_header: Option<String>,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Option<UserInputResponse> {
Expand Down Expand Up @@ -1945,6 +1947,7 @@ async fn stop_event_loop_completes_in_flight_handler() {
&self,
_session_id: SessionId,
_question: String,
_header: Option<String>,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Option<UserInputResponse> {
Expand Down Expand Up @@ -2023,6 +2026,7 @@ async fn drop_session_does_not_abort_handler() {
&self,
_session_id: SessionId,
_question: String,
_header: Option<String>,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Option<UserInputResponse> {
Expand Down
Loading