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
4 changes: 4 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2766,6 +2766,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
1 change: 1 addition & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,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
22 changes: 22 additions & 0 deletions java/src/main/java/com/github/copilot/rpc/SessionMetadata.java
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;
Comment thread
Davsterl marked this conversation as resolved.

@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/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
4 changes: 4 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,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 @@ -1354,6 +1355,7 @@ export class CopilotClient {
startTime: string;
modifiedTime: string;
summary?: string;
clientName?: string;
isRemote: boolean;
context?: { cwd: string; gitRoot?: string; repository?: string; branch?: string };
};
Expand All @@ -1371,6 +1373,7 @@ export class CopilotClient {
startTime: string;
modifiedTime: string;
summary?: string;
clientName?: string;
isRemote: boolean;
context?: { cwd: string; gitRoot?: string; repository?: string; branch?: string };
}): SessionMetadata {
Expand All @@ -1380,6 +1383,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
2 changes: 2 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2033,6 +2033,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 @@ -240,6 +240,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 @@ -780,6 +780,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 @@ -795,6 +796,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 @@ -803,6 +805,7 @@ def from_dict(obj: Any) -> SessionMetadata:
modified_time=_parse_session_timestamp(modified_time),
is_remote=bool(is_remote),
summary=summary,
client_name=client_name,
context=context,
)

Expand All @@ -814,6 +817,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
18 changes: 18 additions & 0 deletions python/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ModelInfo,
ModelLimits,
ModelSupports,
SessionMetadata,
)
from copilot.session import PermissionHandler
from e2e.testharness import CLI_PATH
Expand Down Expand Up @@ -602,6 +603,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/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3362,6 +3362,9 @@ pub struct SessionMetadata {
/// Agent-generated session summary.
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
/// Identifier of the client driving the session.
#[serde(skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
/// Whether the session is running remotely.
pub is_remote: bool,
}
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 @@ -597,6 +597,7 @@ async fn list_sessions_returns_typed_metadata() {
"startTime": "2025-01-01T00:00:00Z",
"modifiedTime": "2025-01-01T01:00:00Z",
"summary": "test session",
"clientName": "my-app",
"isRemote": false,
}]
},
Expand All @@ -607,6 +608,7 @@ async fn list_sessions_returns_typed_metadata() {
assert_eq!(sessions.len(), 1);
assert_eq!(sessions[0].session_id, "s1");
assert_eq!(sessions[0].summary, Some("test session".to_string()));
assert_eq!(sessions[0].client_name.as_deref(), Some("my-app"));
}

#[tokio::test]
Expand Down Expand Up @@ -1100,6 +1102,7 @@ async fn get_session_metadata_returns_typed_metadata() {
"startTime": "2025-01-01T00:00:00Z",
"modifiedTime": "2025-01-01T01:00:00Z",
"summary": "loaded session",
"clientName": "my-app",
"isRemote": false,
}
},
Expand All @@ -1110,6 +1113,7 @@ async fn get_session_metadata_returns_typed_metadata() {
let metadata = metadata.expect("server returned a session");
assert_eq!(metadata.session_id, "s1");
assert_eq!(metadata.summary.as_deref(), Some("loaded session"));
assert_eq!(metadata.client_name.as_deref(), Some("my-app"));
}

#[tokio::test]
Expand Down
Loading