Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import com.microsoft.agenthostprotocol.generated.StateActionSessionInputComplete
import com.microsoft.agenthostprotocol.generated.StateActionSessionInputRequested
import com.microsoft.agenthostprotocol.generated.StateActionSessionIsArchivedChanged
import com.microsoft.agenthostprotocol.generated.StateActionSessionIsReadChanged
import com.microsoft.agenthostprotocol.generated.StateActionSessionMcpServerStateChanged
import com.microsoft.agenthostprotocol.generated.StateActionSessionMetaChanged
import com.microsoft.agenthostprotocol.generated.StateActionSessionModelChanged
import com.microsoft.agenthostprotocol.generated.StateActionSessionPendingMessageRemoved
Expand Down Expand Up @@ -1051,6 +1052,60 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat
}
}

is StateActionSessionMcpServerStateChanged -> {
// Full-replacement of an MCP server customization's `state` + `channel`,
// located by id. Mirrors the canonical TS reducer (and the Go/Rust/Swift
// ports): a top-level McpServer entry is matched first (hosts MAY surface
// MCP servers directly at the top level); otherwise the search descends
// into container children. A no-op when no customization carries the id,
// or when the matched id belongs to a non-MCP customization type.
val a = action.value
val list = state.customizations
if (list == null) {
state
} else {
val topIdx = list.indexOfFirst { customizationId(it) == a.id }
if (topIdx >= 0) {
val entry = list[topIdx]
if (entry !is CustomizationMcpServer) {
state
} else {
val updated = list.toMutableList()
updated[topIdx] = CustomizationMcpServer(
entry.value.copy(state = a.state, channel = a.channel),
)
state.copy(customizations = updated)
}
} else {
var changed = false
val updated = list.map { container ->
val children = customizationChildren(container)
if (children == null) {
container
} else {
val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id }
if (childIdx < 0) {
container
} else {
val child = children[childIdx]
if (child !is ChildCustomizationMcpServer) {
container
} else {
changed = true
val newChildren = children.toMutableList()
newChildren[childIdx] = ChildCustomizationMcpServer(
child.value.copy(state = a.state, channel = a.channel),
)
withCustomizationChildren(container, newChildren)
}
}
}
}
if (!changed) state else state.copy(customizations = updated)
}
}
}

// ── Truncation ────────────────────────────────────────────────────────

is StateActionSessionTruncated -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"description": "session/mcpServerStateChanged replaces the state and channel of a top-level McpServer customization",
"reducer": "session",
"initial": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "mcpServer",
"id": "mcp-1",
"uri": "file:///workspace/.mcp/servers.json",
"name": "Filesystem",
"enabled": true,
"state": { "kind": "starting" }
}
]
},
"actions": [
{
"type": "session/mcpServerStateChanged",
"id": "mcp-1",
"state": { "kind": "ready" },
"channel": "mcp://filesystem"
}
],
"expected": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "mcpServer",
"id": "mcp-1",
"uri": "file:///workspace/.mcp/servers.json",
"name": "Filesystem",
"enabled": true,
"state": { "kind": "ready" },
"channel": "mcp://filesystem"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"description": "session/mcpServerStateChanged replaces the state and channel of an McpServer customization nested inside a container",
"reducer": "session",
"initial": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "plugin",
"id": "plugin-a",
"uri": "https://plugins.example/a",
"name": "Plugin A",
"enabled": true,
"children": [
{
"type": "skill",
"id": "skill-1",
"uri": "https://plugins.example/a#skills/lint",
"name": "lint"
},
{
"type": "mcpServer",
"id": "mcp-child",
"uri": "https://plugins.example/a#mcp/search",
"name": "Search",
"enabled": true,
"state": { "kind": "starting" }
}
]
}
]
},
"actions": [
{
"type": "session/mcpServerStateChanged",
"id": "mcp-child",
"state": { "kind": "ready" },
"channel": "mcp://search"
}
],
"expected": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "plugin",
"id": "plugin-a",
"uri": "https://plugins.example/a",
"name": "Plugin A",
"enabled": true,
"children": [
{
"type": "skill",
"id": "skill-1",
"uri": "https://plugins.example/a#skills/lint",
"name": "lint"
},
{
"type": "mcpServer",
"id": "mcp-child",
"uri": "https://plugins.example/a#mcp/search",
"name": "Search",
"enabled": true,
"state": { "kind": "ready" },
"channel": "mcp://search"
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"description": "session/mcpServerStateChanged is a no-op when no customization carries the targeted id",
"reducer": "session",
"initial": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "mcpServer",
"id": "mcp-1",
"uri": "file:///workspace/.mcp/servers.json",
"name": "Filesystem",
"enabled": true,
"state": { "kind": "ready" },
"channel": "mcp://filesystem"
}
]
},
"actions": [
{
"type": "session/mcpServerStateChanged",
"id": "mcp-does-not-exist",
"state": { "kind": "stopped" }
}
],
"expected": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "mcpServer",
"id": "mcp-1",
"uri": "file:///workspace/.mcp/servers.json",
"name": "Filesystem",
"enabled": true,
"state": { "kind": "ready" },
"channel": "mcp://filesystem"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"description": "session/mcpServerStateChanged is a no-op when the targeted id belongs to a non-McpServer customization (a top-level container or one of its non-MCP children)",
"reducer": "session",
"initial": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "plugin",
"id": "plugin-a",
"uri": "https://plugins.example/a",
"name": "Plugin A",
"enabled": true,
"children": [
{
"type": "skill",
"id": "skill-1",
"uri": "https://plugins.example/a#skills/lint",
"name": "lint"
}
]
}
]
},
"actions": [
{
"type": "session/mcpServerStateChanged",
"id": "plugin-a",
"state": { "kind": "ready" },
"channel": "mcp://nope"
},
{
"type": "session/mcpServerStateChanged",
"id": "skill-1",
"state": { "kind": "ready" },
"channel": "mcp://nope"
}
],
"expected": {
"summary": {
"resource": "copilot:/test-session",
"provider": "copilot",
"title": "Test Session",
"status": 1,
"createdAt": 1000,
"modifiedAt": 1000
},
"lifecycle": "ready",
"turns": [],
"customizations": [
{
"type": "plugin",
"id": "plugin-a",
"uri": "https://plugins.example/a",
"name": "Plugin A",
"enabled": true,
"children": [
{
"type": "skill",
"id": "skill-1",
"uri": "https://plugins.example/a#skills/lint",
"name": "lint"
}
]
}
]
}
}
Loading