Skip to content
Open
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
105 changes: 105 additions & 0 deletions src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,39 @@ pub struct SessionStartData {
pub struct SessionResumeData {
pub resume_time: String,
pub event_count: f64,
/// Optional CLI-supplied workspace context. Copilot CLI 1.0.35+ sends
/// this object alongside the basic resume metadata so downstream
/// consumers (IDE integrations, session dashboards) can rebind to the
/// right working directory, git branch, and remote without re-reading
/// the session store. Older CLI versions omit the field entirely,
/// hence `Option` + `#[serde(default)]` keeps the type backward
/// compatible.
#[serde(default, skip_serializing_if = "Option::is_none")]
Comment on lines +126 to +129
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

context is an Option, so missing JSON already deserializes to None without #[serde(default)]. The doc comment also implies #[serde(default)] is needed for backward compatibility, which isn’t true for Option fields. Consider dropping default here (keeping skip_serializing_if) and adjusting the comment accordingly.

Suggested change
/// the session store. Older CLI versions omit the field entirely,
/// hence `Option` + `#[serde(default)]` keeps the type backward
/// compatible.
#[serde(default, skip_serializing_if = "Option::is_none")]
/// the session store. Older CLI versions may omit the field entirely,
/// and `Option` keeps deserialization backward compatible in that case.
#[serde(skip_serializing_if = "Option::is_none")]

Copilot uses AI. Check for mistakes.
pub context: Option<SessionResumeContext>,
}

/// Workspace context payload attached to a `session.resume` event by the
/// Copilot CLI. Every field is optional because the CLI may omit any
/// subset depending on whether the resumed session lives inside a git
/// repository and which host (IDE / terminal / etc.) initiated the
/// resume.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionResumeContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git_root: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
Comment on lines +141 to +153
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

In SessionResumeContext, each field is Option<...> and will already default to None when omitted; the per-field #[serde(default)] is redundant and inconsistent with other Option fields in this file (which typically only use skip_serializing_if). Consider removing default on these fields to reduce annotation noise.

Suggested change
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git_root: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_root: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]

Copilot uses AI. Check for mistakes.
pub host_type: Option<String>,
}

/// Data for session.error event.
Expand Down Expand Up @@ -1447,4 +1480,76 @@ mod tests {
let event = SessionEvent::from_json(&json).unwrap();
assert_eq!(event.event_type, "session.usage_info");
}

/// Regression fixture derived from the Copilot CLI 1.0.35 wire trace
/// captured during DevInSip POC-3 (see docs/POC_COPILOT_SDK.md in the
/// DevInSip repo). The CLI emits `session.resume` with an additional
/// `context` object that the pre-patch SDK silently dropped, leaving
/// downstream consumers (session binding, sidebar cwd) unable to
/// recover the working directory without bypassing the SDK.
Comment on lines +1484 to +1489
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The regression-test doc comment references an external repo/POC document (“DevInSip POC-3”, “docs/POC_COPILOT_SDK.md”). That makes the test less self-contained and can become confusing/stale for future maintainers. Consider rewriting this comment to describe the provenance generically (e.g., “captured from Copilot CLI 1.0.35 wire trace”) without external repo names/paths.

Suggested change
/// Regression fixture derived from the Copilot CLI 1.0.35 wire trace
/// captured during DevInSip POC-3 (see docs/POC_COPILOT_SDK.md in the
/// DevInSip repo). The CLI emits `session.resume` with an additional
/// `context` object that the pre-patch SDK silently dropped, leaving
/// downstream consumers (session binding, sidebar cwd) unable to
/// recover the working directory without bypassing the SDK.
/// Regression fixture derived from a Copilot CLI 1.0.35 wire trace.
/// The CLI emits `session.resume` with an additional `context` object
/// that the pre-patch SDK silently dropped, leaving downstream
/// consumers (session binding, sidebar cwd) unable to recover the
/// working directory without bypassing the SDK.

Copilot uses AI. Check for mistakes.
#[test]
fn test_session_resume_preserves_cli_context() {
let json = json!({
"id": "evt_resume_1",
"timestamp": "2026-04-24T00:00:00Z",
"type": "session.resume",
"data": {
"resumeTime": "2026-04-24T00:00:00Z",
"eventCount": 42.0,
"context": {
"cwd": "/Users/dev/project",
"gitRoot": "/Users/dev/project",
"branch": "main",
"baseCommit": "abc123",
"headCommit": "def456",
"repository": "github.com/org/repo",
"hostType": "vscode"
}
}
});

let event = SessionEvent::from_json(&json).unwrap();
assert_eq!(event.event_type, "session.resume");

let resume = match &event.data {
SessionEventData::SessionResume(r) => r,
other => panic!("expected SessionResume variant, got {other:?}"),
};
assert_eq!(resume.resume_time, "2026-04-24T00:00:00Z");
assert_eq!(resume.event_count, 42.0);

let ctx = resume
.context
.as_ref()
.expect("context must round-trip through SessionResumeData");
assert_eq!(ctx.cwd.as_deref(), Some("/Users/dev/project"));
Comment on lines +1521 to +1525
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This test’s comments/error message say the context must “round-trip”, but the test only verifies deserialization (from_json). Either extend the test to serialize back to JSON and re-parse, or adjust the wording to avoid implying serialization coverage.

Copilot uses AI. Check for mistakes.
assert_eq!(ctx.git_root.as_deref(), Some("/Users/dev/project"));
assert_eq!(ctx.branch.as_deref(), Some("main"));
assert_eq!(ctx.base_commit.as_deref(), Some("abc123"));
assert_eq!(ctx.head_commit.as_deref(), Some("def456"));
assert_eq!(ctx.repository.as_deref(), Some("github.com/org/repo"));
assert_eq!(ctx.host_type.as_deref(), Some("vscode"));
}

/// CLI payloads that omit `context` must still deserialize successfully
/// — the field is additive and backward-compatible.
#[test]
fn test_session_resume_without_context_is_backward_compatible() {
let json = json!({
"id": "evt_resume_2",
"timestamp": "2026-04-24T00:00:00Z",
"type": "session.resume",
"data": {
"resumeTime": "2026-04-24T00:00:00Z",
"eventCount": 0.0
}
});

let event = SessionEvent::from_json(&json).unwrap();
let resume = match &event.data {
SessionEventData::SessionResume(r) => r,
other => panic!("expected SessionResume variant, got {other:?}"),
};
assert!(resume.context.is_none());
}
}
Loading