Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions dotnet/src/Generated/Rpc.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2667,6 +2667,10 @@ public sealed class SessionMetadata
/// </summary>
public string? Summary { get; set; }
/// <summary>
/// Identifier of the client driving the session.
/// </summary>
public string? ClientName { get; set; }
/// <summary>
/// Whether the session is running on a remote server.
/// </summary>
public bool IsRemote { get; set; }
Expand Down
24 changes: 24 additions & 0 deletions dotnet/test/Unit/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,30 @@ public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptio
Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString());
}

[Fact]
public void SessionMetadata_CanRoundTripClientName_WithSdkOptions()
{
var options = GetSerializerOptions();
var original = new SessionMetadata
{
SessionId = "session-1",
StartTime = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
ModifiedTime = DateTimeOffset.Parse("2025-01-01T01:00:00Z"),
Summary = "loaded session",
ClientName = "my-app",
IsRemote = false
};

var json = JsonSerializer.Serialize(original, options);
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
Assert.Equal("my-app", root.GetProperty("clientName").GetString());

var deserialized = JsonSerializer.Deserialize<SessionMetadata>(json, options);
Assert.NotNull(deserialized);
Assert.Equal("my-app", deserialized.ClientName);
}

[Fact]
public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions()
{
Expand Down
27 changes: 27 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,33 @@ func TestResumeSessionRequest_ClientName(t *testing.T) {
})
}

func TestSessionMetadata_ClientName(t *testing.T) {
t.Run("round-trips clientName in JSON", func(t *testing.T) {
var metadata SessionMetadata
if err := json.Unmarshal([]byte(`{
"sessionId":"s1",
"startTime":"2025-01-01T00:00:00Z",
"modifiedTime":"2025-01-01T01:00:00Z",
"summary":"loaded session",
"clientName":"my-app",
"isRemote":false
}`), &metadata); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if metadata.ClientName == nil || *metadata.ClientName != "my-app" {
t.Fatalf("Expected clientName to be my-app, got %v", metadata.ClientName)
}

data, err := json.Marshal(metadata)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
if !strings.Contains(string(data), `"clientName":"my-app"`) {
t.Fatalf("Expected marshaled JSON to include clientName, got %s", string(data))
}
})
}

func TestCreateSessionRequest_Agent(t *testing.T) {
t.Run("includes agent in JSON when set", func(t *testing.T) {
req := createSessionRequest{Agent: "test-agent"}
Expand Down
2 changes: 2 additions & 0 deletions go/rpc/zrpc.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,7 @@ type SessionMetadata struct {
StartTime time.Time `json:"startTime"`
ModifiedTime time.Time `json:"modifiedTime"`
Summary *string `json:"summary,omitempty"`
ClientName *string `json:"clientName,omitempty"`
IsRemote bool `json:"isRemote"`
Context *SessionContext `json:"context,omitempty"`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public record SessionMetadata(
@JsonProperty("summary") String summary,
/** Optional human-friendly name set via /rename */
@JsonProperty("name") String name,
/** Runtime client name that created/last resumed this session */
@JsonProperty("clientName") String clientName,
/** True for remote (GitHub) sessions; false for local */
@JsonProperty("isRemote") Boolean isRemote,
/** Schema for the `SessionContext` type. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public class SessionMetadata {
@JsonProperty("summary")
private String summary;

@JsonProperty("clientName")
private String clientName;

@JsonProperty("isRemote")
private boolean isRemote;

Expand Down Expand Up @@ -130,6 +133,25 @@ public void setSummary(String summary) {
this.summary = summary;
}

/**
* Gets the identifier of the client driving the session.
*
* @return the client name, or {@code null} if not available
*/
public String getClientName() {
return clientName;
}

/**
* Sets the client identifier for the session.
*
* @param clientName
* the client name
*/
public void setClientName(String clientName) {
this.clientName = clientName;
}

/**
* Returns whether this session is stored remotely.
*
Expand Down
3 changes: 3 additions & 0 deletions java/src/test/java/com/github/copilot/sdk/ModelInfoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ void sessionMetadataGettersAndSetters() {
assertNull(meta.getStartTime());
assertNull(meta.getModifiedTime());
assertNull(meta.getSummary());
assertNull(meta.getClientName());
assertFalse(meta.isRemote());

meta.setClientName("my-app");
assertEquals("my-app", meta.getClientName());
meta.setRemote(true);
assertTrue(meta.isRemote());
}
Expand Down
3 changes: 3 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,7 @@ export class CopilotClient {
startTime: string;
modifiedTime: string;
summary?: string;
clientName?: string;
isRemote: boolean;
context?: { cwd: string; gitRoot?: string; repository?: string; branch?: string };
}>;
Expand Down Expand Up @@ -1402,6 +1403,7 @@ export class CopilotClient {
startTime: string;
modifiedTime: string;
summary?: string;
clientName?: string;
isRemote: boolean;
Comment thread
Davsterl marked this conversation as resolved.
context?: { cwd: string; gitRoot?: string; repository?: string; branch?: string };
}): SessionMetadata {
Expand All @@ -1411,6 +1413,7 @@ export class CopilotClient {
startTime: new Date(raw.startTime),
modifiedTime: new Date(raw.modifiedTime),
summary: raw.summary,
clientName: raw.clientName,
isRemote: raw.isRemote,
context: context
? {
Expand Down
4 changes: 4 additions & 0 deletions nodejs/src/generated/rpc.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1970,6 +1970,8 @@ export interface SessionMetadata {
startTime: Date;
modifiedTime: Date;
summary?: string;
/** Identifier of the client driving the session */
clientName?: string;
isRemote: boolean;
/** Working directory context (working directory, git info) from session creation */
context?: SessionContext;
Expand Down
74 changes: 74 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,80 @@ describe("CopilotClient", () => {
spy.mockRestore();
});

it("maps clientName from session.list responses", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

vi.spyOn((client as any).connection!, "sendRequest").mockImplementation(
async (method: string) => {
if (method === "session.list") {
return {
sessions: [
{
sessionId: "session-1",
startTime: "2025-01-01T00:00:00Z",
modifiedTime: "2025-01-01T01:00:00Z",
summary: "test session",
clientName: "my-app",
isRemote: false,
},
],
};
}
throw new Error(`Unexpected method: ${method}`);
}
);

const sessions = await client.listSessions();

expect(sessions).toEqual([
expect.objectContaining({
sessionId: "session-1",
clientName: "my-app",
summary: "test session",
}),
]);
expect(sessions[0].startTime).toBeInstanceOf(Date);
expect(sessions[0].modifiedTime).toBeInstanceOf(Date);
});

it("maps clientName from session.getMetadata responses", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

vi.spyOn((client as any).connection!, "sendRequest").mockImplementation(
async (method: string) => {
if (method === "session.getMetadata") {
return {
session: {
sessionId: "session-1",
startTime: "2025-01-01T00:00:00Z",
modifiedTime: "2025-01-01T01:00:00Z",
summary: "loaded session",
clientName: "my-app",
isRemote: false,
},
};
}
throw new Error(`Unexpected method: ${method}`);
}
);

const metadata = await client.getSessionMetadata("session-1");

expect(metadata).toEqual(
expect.objectContaining({
sessionId: "session-1",
clientName: "my-app",
summary: "loaded session",
})
);
expect(metadata?.startTime).toBeInstanceOf(Date);
expect(metadata?.modifiedTime).toBeInstanceOf(Date);
});

it("forwards enableSessionTelemetry in session.create request", async () => {
const client = new CopilotClient();
await client.start();
Expand Down
5 changes: 5 additions & 0 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@ class SessionMetadata:
modified_time: datetime # Timestamp when session was last modified
is_remote: bool # Whether the session is remote
summary: str | None = None # Optional summary of the session
client_name: str | None = None # Identifier of the client driving the session
context: SessionContext | None = None # Working directory context

@staticmethod
Expand All @@ -799,6 +800,7 @@ def from_dict(obj: Any) -> SessionMetadata:
f"startTime={start_time}, modifiedTime={modified_time}, isRemote={is_remote}"
)
summary = obj.get("summary")
client_name = obj.get("clientName")
context_dict = obj.get("context")
context = SessionContext.from_dict(context_dict) if context_dict else None
return SessionMetadata(
Expand All @@ -807,6 +809,7 @@ def from_dict(obj: Any) -> SessionMetadata:
modified_time=_parse_session_timestamp(modified_time),
is_remote=bool(is_remote),
summary=summary,
client_name=str(client_name) if client_name is not None else None,
Comment thread
Davsterl marked this conversation as resolved.
Outdated
context=context,
)

Expand All @@ -818,6 +821,8 @@ def to_dict(self) -> dict:
result["isRemote"] = self.is_remote
if self.summary is not None:
result["summary"] = self.summary
if self.client_name is not None:
result["clientName"] = self.client_name
if self.context is not None:
result["context"] = self.context.to_dict()
return result
Expand Down
8 changes: 7 additions & 1 deletion python/copilot/generated/rpc.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions python/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ModelInfo,
ModelLimits,
ModelSupports,
SessionMetadata,
)
from copilot.session import PermissionHandler
from e2e.testharness import CLI_PATH
Expand Down Expand Up @@ -601,6 +602,23 @@ async def mock_request(method, params):
finally:
await client.force_stop()


class TestSessionMetadataParsing:
def test_session_metadata_round_trips_client_name(self):
metadata = SessionMetadata.from_dict(
{
"sessionId": "session-1",
"startTime": "2025-01-01T00:00:00Z",
"modifiedTime": "2025-01-01T01:00:00Z",
"summary": "loaded session",
"clientName": "my-app",
"isRemote": False,
}
)

assert metadata.client_name == "my-app"
assert metadata.to_dict()["clientName"] == "my-app"

@pytest.mark.asyncio
async def test_create_session_forwards_provider_headers(self):
client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
Expand Down
3 changes: 3 additions & 0 deletions rust/src/generated/api_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5863,6 +5863,9 @@ pub struct SessionMetadata {
/// GitHub task ID, when this local session is bound to one. Only present for local sessions exported to remote control.
#[serde(skip_serializing_if = "Option::is_none")]
pub mc_task_id: Option<String>,
/// Identifier of the client driving the session.
#[serde(skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
/// Last-modified time of the session's persisted state, as ISO 8601
pub modified_time: String,
/// Optional human-friendly name set via /rename
Expand Down
Loading
Loading