From d506e3d1c69969ca817207882cc3673b9e356c14 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:27:37 -0600 Subject: [PATCH 1/4] Show MCP agent info in Activity Log Add `isMCPAgent` and `mcpClient` properties to `ActivityActor` to surface when activity was performed by an MCP agent. Display the MCP client name (e.g. "via Claude") in both the activity list rows and detail view. Co-Authored-By: Claude Opus 4.6 --- Modules/Sources/WordPressKit/Activity.swift | 4 + .../ActivityStringFormattingTests.swift | 118 ++++++++++++ .../Tests/ActivityTests.swift | 173 +++++++++++++++++- .../Activity/ActivityStringFormatting.swift | 40 ++++ .../ActivityLogDetailsView+Preview.swift | 40 ++++ .../Details/ActivityLogDetailsView.swift | 19 +- .../Activity/List/ActivityLogRowView.swift | 9 + .../List/ActivityLogRowViewModel.swift | 16 +- 8 files changed, 404 insertions(+), 15 deletions(-) create mode 100644 Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift create mode 100644 WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift diff --git a/Modules/Sources/WordPressKit/Activity.swift b/Modules/Sources/WordPressKit/Activity.swift index 55851f239a5c..f509be9e8e35 100644 --- a/Modules/Sources/WordPressKit/Activity.swift +++ b/Modules/Sources/WordPressKit/Activity.swift @@ -121,6 +121,8 @@ public struct ActivityActor { public let wpcomUserID: String public let avatarURL: String public let role: String + public let isMCPAgent: Bool + public let mcpClient: String? init(dictionary: [String: Any]) { displayName = dictionary["name"] as? String ?? "" @@ -132,6 +134,8 @@ public struct ActivityActor { avatarURL = "" } role = dictionary["role"] as? String ?? "" + isMCPAgent = dictionary["is_mcp_agent"] as? Bool ?? false + mcpClient = dictionary["mcp_client"] as? String } public lazy var isJetpack: Bool = { diff --git a/Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift b/Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift new file mode 100644 index 000000000000..d4801b83463a --- /dev/null +++ b/Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift @@ -0,0 +1,118 @@ +@testable import WordPress +@testable import WordPressKit +import Testing + +struct ActivityStringFormattingTests { + + // MARK: - actorName + + @Test func actorNameReturnsDisplayName() { + let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "administrator"]) + #expect(ActivityStringFormatting.actorName(for: actor) == "Alice") + } + + @Test func actorNameReturnsUnknownUserWhenEmpty() { + let actor = ActivityActor(dictionary: ["name": "", "type": "Person", "role": "editor"]) + #expect(ActivityStringFormatting.actorName(for: actor) == Activity.Strings.unknownUser) + } + + @Test func actorNameReturnsUnknownUserWhenMissing() { + let actor = ActivityActor(dictionary: ["type": "Person", "role": "editor"]) + #expect(ActivityStringFormatting.actorName(for: actor) == Activity.Strings.unknownUser) + } + + // MARK: - actorRole + + @Test func actorRoleReturnsCapitalizedRole() { + let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "administrator"]) + #expect(ActivityStringFormatting.actorRole(for: actor) == "Administrator") + } + + @Test func actorRoleFallsBackToTypeWhenRoleEmpty() { + let actor = ActivityActor(dictionary: ["name": "Jetpack", "type": "Application", "role": ""]) + #expect(ActivityStringFormatting.actorRole(for: actor) == "Application") + } + + @Test func actorRoleFallsBackToTypeWhenRoleMissing() { + let actor = ActivityActor(dictionary: ["name": "Jetpack", "type": "Application"]) + #expect(ActivityStringFormatting.actorRole(for: actor) == "Application") + } + + // MARK: - actorDescription + + @Test func actorDescriptionForMCPAgent() { + let actor = ActivityActor(dictionary: [ + "name": "bot-user", + "type": "Person", + "role": "administrator", + "is_mcp_agent": true, + "mcp_client": "Claude" + ]) + #expect(ActivityStringFormatting.actorDescription(for: actor) == "Administrator via Claude") + } + + @Test func actorDescriptionForNonMCPActor() { + let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "editor"]) + #expect(ActivityStringFormatting.actorDescription(for: actor) == "Editor") + } + + @Test func actorDescriptionForMCPAgentWithEmptyClient() { + let actor = ActivityActor(dictionary: [ + "name": "bot-user", + "type": "Person", + "role": "administrator", + "is_mcp_agent": true, + "mcp_client": "" + ]) + #expect(ActivityStringFormatting.actorDescription(for: actor) == "Administrator") + } + + @Test func actorDescriptionForMCPAgentWithNoClient() { + let actor = ActivityActor(dictionary: [ + "name": "bot-user", + "type": "Person", + "role": "administrator", + "is_mcp_agent": true + ]) + #expect(ActivityStringFormatting.actorDescription(for: actor) == "Administrator") + } + + // MARK: - botName + + @Test func botNameForMCPAgent() { + let actor = ActivityActor(dictionary: [ + "name": "bot-user", + "type": "Person", + "role": "administrator", + "is_mcp_agent": true, + "mcp_client": "Claude" + ]) + #expect(ActivityStringFormatting.botName(for: actor) == "via Claude") + } + + @Test func botNameForNonMCPActor() { + let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "editor"]) + #expect(ActivityStringFormatting.botName(for: actor) == nil) + } + + @Test func botNameForMCPAgentWithEmptyClient() { + let actor = ActivityActor(dictionary: [ + "name": "bot-user", + "type": "Person", + "role": "administrator", + "is_mcp_agent": true, + "mcp_client": "" + ]) + #expect(ActivityStringFormatting.botName(for: actor) == nil) + } + + @Test func botNameForMCPAgentWithNoClient() { + let actor = ActivityActor(dictionary: [ + "name": "bot-user", + "type": "Person", + "role": "administrator", + "is_mcp_agent": true + ]) + #expect(ActivityStringFormatting.botName(for: actor) == nil) + } +} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/ActivityTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/ActivityTests.swift index a1b6c09eede5..c5953685975f 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/ActivityTests.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/ActivityTests.swift @@ -1,20 +1,173 @@ @testable import WordPressKit -import XCTest +import Testing -class ActivityTests: XCTestCase { +struct ActivityTests { - func testActivityDecoding() throws { - let data = try XCTUnwrap(activityLogComment.data(using: .utf8)) - // This is part of the test in itself. - // If Activity is not configured as expected to decode the JSON input, JSONDecode will throw. + @Test func activityDecoding() throws { + let data = try #require(activityLogComment.data(using: .utf8)) let activity = try JSONDecoder().decode(Activity.self, from: data) - // Verify custom keys - XCTAssertEqual(activity.activityID, "AWRNRTAUjEqjFGbx8DZj") - XCTAssertFalse(activity.isRewindable) - XCTAssertEqual(activity.rewindID, "1530304735.2771") + #expect(activity.activityID == "AWRNRTAUjEqjFGbx8DZj") + #expect(activity.isRewindable == false) + #expect(activity.rewindID == "1530304735.2771") + } + + @Test func actorWithMCPAgentFields() throws { + let data = try #require(activityLogMCPAgent.data(using: .utf8)) + let activity = try JSONDecoder().decode(Activity.self, from: data) + let actor = try #require(activity.actor) + #expect(actor.isMCPAgent == true) + #expect(actor.mcpClient == "Claude") + #expect(actor.displayName == "bot-user") + #expect(actor.role == "administrator") + } + + @Test func actorWithoutMCPAgentFields() throws { + let data = try #require(activityLogComment.data(using: .utf8)) + let activity = try JSONDecoder().decode(Activity.self, from: data) + let actor = try #require(activity.actor) + #expect(actor.isMCPAgent == false) + #expect(actor.mcpClient == nil) + } + + @Test func actorWithExplicitlyFalseMCPAgent() throws { + let data = try #require(activityLogExplicitlyNotMCPAgent.data(using: .utf8)) + let activity = try JSONDecoder().decode(Activity.self, from: data) + let actor = try #require(activity.actor) + #expect(actor.isMCPAgent == false) + #expect(actor.mcpClient == nil) + } + + @Test func actorWithMCPAgentButNoClient() throws { + let data = try #require(activityLogMCPAgentNoClient.data(using: .utf8)) + let activity = try JSONDecoder().decode(Activity.self, from: data) + let actor = try #require(activity.actor) + #expect(actor.isMCPAgent == true) + #expect(actor.mcpClient == nil) } } +private let activityLogMCPAgent: String = """ +{ + "summary": "Post published", + "content": { + "text": "Post published by MCP agent" + }, + "name": "post__published", + "actor": { + "type": "Person", + "name": "bot-user", + "external_user_id": 0, + "wpcom_user_id": 100000001, + "icon": { + "type": "Image", + "url": "https://secure.gravatar.com/avatar/example?s=96&d=identicon&r=g", + "width": 96, + "height": 96 + }, + "role": "administrator", + "is_mcp_agent": true, + "mcp_client": "Claude" + }, + "type": "Create", + "published": "2026-03-17T10:00:00.000+00:00", + "generator": { + "jetpack_version": 0, + "blog_id": 137726971 + }, + "is_rewindable": false, + "rewind_id": "1710000000.0001", + "gridicon": "posts", + "status": "success", + "activity_id": "test-mcp-agent-activity", + "object": { + "type": "Post", + "object_id": 100 + }, + "is_discarded": false +} +""" + +private let activityLogExplicitlyNotMCPAgent: String = """ +{ + "summary": "Post updated", + "content": { + "text": "Post updated by a human" + }, + "name": "post__updated", + "actor": { + "type": "Person", + "name": "human-user", + "external_user_id": 0, + "wpcom_user_id": 100000002, + "icon": { + "type": "Image", + "url": "https://secure.gravatar.com/avatar/example2?s=96&d=identicon&r=g", + "width": 96, + "height": 96 + }, + "role": "editor", + "is_mcp_agent": false, + "mcp_client": null + }, + "type": "Update", + "published": "2026-03-17T11:00:00.000+00:00", + "generator": { + "jetpack_version": 0, + "blog_id": 137726971 + }, + "is_rewindable": false, + "rewind_id": "1710000001.0001", + "gridicon": "posts", + "status": "success", + "activity_id": "test-not-mcp-agent", + "object": { + "type": "Post", + "object_id": 101 + }, + "is_discarded": false +} +""" + +private let activityLogMCPAgentNoClient: String = """ +{ + "summary": "Post drafted", + "content": { + "text": "Post drafted by MCP agent without client" + }, + "name": "post__drafted", + "actor": { + "type": "Person", + "name": "agent-user", + "external_user_id": 0, + "wpcom_user_id": 100000003, + "icon": { + "type": "Image", + "url": "https://secure.gravatar.com/avatar/example3?s=96&d=identicon&r=g", + "width": 96, + "height": 96 + }, + "role": "administrator", + "is_mcp_agent": true + }, + "type": "Create", + "published": "2026-03-17T12:00:00.000+00:00", + "generator": { + "jetpack_version": 0, + "blog_id": 137726971 + }, + "is_rewindable": false, + "rewind_id": "1710000002.0001", + "gridicon": "posts", + "status": "success", + "activity_id": "test-mcp-agent-no-client", + "object": { + "type": "Post", + "object_id": 102 + }, + "is_discarded": false +} +""" + // See https://github.com/wordpress-mobile/WordPress-iOS/blob/16adc688f718136ea57c45d5d26c5c13de9d2b9f/WordPress/WordPressTest/Test%20Data/activity-log-comment.json private let activityLogComment: String = """ { diff --git a/WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift b/WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift new file mode 100644 index 000000000000..5b67d2a53472 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift @@ -0,0 +1,40 @@ +import Foundation +import WordPressData + +struct ActivityStringFormatting { + private static let agentString = NSLocalizedString( + "activityDetail.section.agent", + value: "via %@", + comment: "Explanation of the actor chain for a given operation (ex: via Claude Code)" + ) + + private static let combinedAgentString = NSLocalizedString( + "activityDetail.section.actorAndAgent", + value: "%@ via %@", + comment: "Explanation of the actor chain for a given operation (ex: Bob via Claude Code)" + ) + + static func actorName(for actor: ActivityActor) -> String { + actor.displayName.isEmpty ? Activity.Strings.unknownUser : actor.displayName + } + + static func actorRole(for actor: ActivityActor) -> String { + actor.role.isEmpty ? actor.type.localizedCapitalized : actor.role.localizedCapitalized + } + + static func actorDescription(for actor: ActivityActor) -> String { + if actor.isMCPAgent, let mcpClient = actor.mcpClient, !mcpClient.isEmpty { + return String(format: Self.combinedAgentString, actorRole(for: actor), mcpClient) + } else { + return actorRole(for: actor) + } + } + + static func botName(for actor: ActivityActor) -> String? { + guard actor.isMCPAgent, let mcpClient = actor.mcpClient, !mcpClient.isEmpty else { + return nil + } + + return String(format: Self.agentString, mcpClient) + } +} diff --git a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView+Preview.swift b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView+Preview.swift index fcb6a186d27d..9a4d701e510e 100644 --- a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView+Preview.swift +++ b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView+Preview.swift @@ -62,6 +62,46 @@ extension ActivityLogDetailsView { return try! JSONDecoder().decode(Activity.self, from: json.data(using: .utf8)!) } + static var mockMCPAgentActivity: Activity { + let json = """ + { + "summary": "Post published", + "content": { + "text": "Published blog post: Getting Started with MCP" + }, + "name": "post__published", + "actor": { + "type": "Person", + "name": "bot-user", + "external_user_id": 0, + "wpcom_user_id": 100000001, + "icon": { + "type": "Image", + "url": "", + "width": 96, + "height": 96 + }, + "role": "administrator", + "is_mcp_agent": true, + "mcp_client": "Claude" + }, + "type": "Create", + "published": "2026-03-17T10:00:00.000+00:00", + "generator": { + "jetpack_version": 0, + "blog_id": 137726971 + }, + "is_rewindable": false, + "rewind_id": "1710000000.0001", + "gridicon": "posts", + "status": "success", + "activity_id": "mock-mcp-agent-activity", + "is_discarded": false + } + """ + return try! JSONDecoder().decode(Activity.self, from: json.data(using: .utf8)!) + } + static var mockLoginActivity: Activity { let json = """ { diff --git a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift index 0603ab6238b1..69358b829281 100644 --- a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift +++ b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift @@ -48,12 +48,18 @@ struct ActivityLogDetailsView: View { // Actor info VStack(alignment: .leading, spacing: 2) { - Text(actor.displayName.isEmpty ? Activity.Strings.unknownUser : actor.displayName) + Text(ActivityStringFormatting.actorName(for: actor)) .font(.headline) - Text(actor.role.isEmpty ? actor.type.localizedCapitalized : actor.role.localizedCapitalized) + Text(ActivityStringFormatting.actorRole(for: actor)) .font(.subheadline) .foregroundStyle(.secondary) + + if let botName = ActivityStringFormatting.botName(for: actor) { + Text(botName) + .font(.caption) + .foregroundStyle(.secondary) + } } Spacer() @@ -188,6 +194,15 @@ private struct ActivityHeaderView: View { } } +#Preview("MCP Agent") { + NavigationView { + ActivityLogDetailsView( + activity: ActivityLogDetailsView.Mocks.mockMCPAgentActivity, + blog: Blog.mock + ) + } +} + // MARK: - Localized Strings private enum Strings { diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift index 251f4a801468..ae20159e5b17 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift @@ -53,6 +53,15 @@ struct ActivityLogRowView: View { .font(.footnote) .foregroundColor(.secondary) } + + if let metadata = viewModel.actorMetadata { + Text("ยท") + .font(.footnote) + .foregroundColor(.secondary) + Text(metadata) + .font(.footnote) + .foregroundColor(.secondary) + } } } } diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift index 915e9ac8f8ca..01f706db3724 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift @@ -7,7 +7,8 @@ import FormattableContentKit struct ActivityLogRowViewModel: Identifiable { let id: String - var actorSubtitle: String? + let actorSubtitle: String? + let actorMetadata: String? let title: String let subtitle: String let date: Date @@ -19,11 +20,20 @@ struct ActivityLogRowViewModel: Identifiable { init(activity: Activity) { self.activity = activity self.id = activity.activityID + if let actor = activity.actor { - if !actor.role.isEmpty { - actorSubtitle = actor.role.localizedCapitalized + actorSubtitle = ActivityStringFormatting.actorRole(for: actor) + if let botName = ActivityStringFormatting.botName(for: actor) { + actorMetadata = botName + } + else { + actorMetadata = nil } + } else { + actorSubtitle = nil + actorMetadata = nil } + self.date = activity.published self.time = activity.published.formatted(date: .omitted, time: .shortened) self.title = activity.summary.localizedCapitalized From 5d175c7b8b37ee0933909e30973fe6e479200dff Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:27:43 -0600 Subject: [PATCH 2/4] Fix Gutenberg xcframeworks copied into nested version directory `cp -a` copies the source directory itself into the destination when it already exists, creating `WordPress/Frameworks/v1.121.0/`. Copy contents instead so frameworks land directly in `WordPress/Frameworks/`. Co-Authored-By: Claude Opus 4.6 --- Scripts/download-gutenberg-xcframeworks.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Scripts/download-gutenberg-xcframeworks.sh b/Scripts/download-gutenberg-xcframeworks.sh index 66a7e2fc6dce..2019b1bbd1eb 100755 --- a/Scripts/download-gutenberg-xcframeworks.sh +++ b/Scripts/download-gutenberg-xcframeworks.sh @@ -52,6 +52,7 @@ if [[ -z "${FRAMEWORKS_DIR}" || "${FRAMEWORKS_DIR}" == "/" ]]; then echo "Error: invalid frameworks directory: '${FRAMEWORKS_DIR}'" >&2 exit 1 fi -cp -a "${CACHE_DIR}" "${FRAMEWORKS_DIR}" +mkdir -p "${FRAMEWORKS_DIR}" +cp -a "${CACHE_DIR}/"* "${FRAMEWORKS_DIR}/" echo "Gutenberg ${VERSION} setup complete." From 3e9c1800904333a45fe4b2ec98369f0ab0134151 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:41:54 -0600 Subject: [PATCH 3/4] Fix review issues in MCP agent activity log PR - Use positional format specifier (%1$@) for localized string - Remove unused actorDescription(for:) method and its tests - Restore original actorSubtitle behavior for non-MCP rows - Fix shell script to copy dotfiles using cp -a dir/. pattern - Add lineLimit(1) to metadata text to prevent truncation - Improve localization comment for translator clarity Co-Authored-By: Claude Opus 4.6 --- Scripts/download-gutenberg-xcframeworks.sh | 2 +- .../ActivityStringFormattingTests.swift | 39 ------------------- .../Activity/ActivityStringFormatting.swift | 18 +-------- .../Activity/List/ActivityLogRowView.swift | 1 + .../List/ActivityLogRowViewModel.swift | 9 +---- 5 files changed, 6 insertions(+), 63 deletions(-) diff --git a/Scripts/download-gutenberg-xcframeworks.sh b/Scripts/download-gutenberg-xcframeworks.sh index 2019b1bbd1eb..a1f2ade8c49a 100755 --- a/Scripts/download-gutenberg-xcframeworks.sh +++ b/Scripts/download-gutenberg-xcframeworks.sh @@ -53,6 +53,6 @@ if [[ -z "${FRAMEWORKS_DIR}" || "${FRAMEWORKS_DIR}" == "/" ]]; then exit 1 fi mkdir -p "${FRAMEWORKS_DIR}" -cp -a "${CACHE_DIR}/"* "${FRAMEWORKS_DIR}/" +cp -a "${CACHE_DIR}/." "${FRAMEWORKS_DIR}/" echo "Gutenberg ${VERSION} setup complete." diff --git a/Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift b/Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift index d4801b83463a..6e2076b59d62 100644 --- a/Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Activity/ActivityStringFormattingTests.swift @@ -38,45 +38,6 @@ struct ActivityStringFormattingTests { #expect(ActivityStringFormatting.actorRole(for: actor) == "Application") } - // MARK: - actorDescription - - @Test func actorDescriptionForMCPAgent() { - let actor = ActivityActor(dictionary: [ - "name": "bot-user", - "type": "Person", - "role": "administrator", - "is_mcp_agent": true, - "mcp_client": "Claude" - ]) - #expect(ActivityStringFormatting.actorDescription(for: actor) == "Administrator via Claude") - } - - @Test func actorDescriptionForNonMCPActor() { - let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "editor"]) - #expect(ActivityStringFormatting.actorDescription(for: actor) == "Editor") - } - - @Test func actorDescriptionForMCPAgentWithEmptyClient() { - let actor = ActivityActor(dictionary: [ - "name": "bot-user", - "type": "Person", - "role": "administrator", - "is_mcp_agent": true, - "mcp_client": "" - ]) - #expect(ActivityStringFormatting.actorDescription(for: actor) == "Administrator") - } - - @Test func actorDescriptionForMCPAgentWithNoClient() { - let actor = ActivityActor(dictionary: [ - "name": "bot-user", - "type": "Person", - "role": "administrator", - "is_mcp_agent": true - ]) - #expect(ActivityStringFormatting.actorDescription(for: actor) == "Administrator") - } - // MARK: - botName @Test func botNameForMCPAgent() { diff --git a/WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift b/WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift index 5b67d2a53472..fad2dced2f21 100644 --- a/WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift +++ b/WordPress/Classes/ViewRelated/Activity/ActivityStringFormatting.swift @@ -4,14 +4,8 @@ import WordPressData struct ActivityStringFormatting { private static let agentString = NSLocalizedString( "activityDetail.section.agent", - value: "via %@", - comment: "Explanation of the actor chain for a given operation (ex: via Claude Code)" - ) - - private static let combinedAgentString = NSLocalizedString( - "activityDetail.section.actorAndAgent", - value: "%@ via %@", - comment: "Explanation of the actor chain for a given operation (ex: Bob via Claude Code)" + value: "via %1$@", + comment: "Shows the MCP client used for an activity. %1$@ is the MCP client name (e.g. Claude)." ) static func actorName(for actor: ActivityActor) -> String { @@ -22,14 +16,6 @@ struct ActivityStringFormatting { actor.role.isEmpty ? actor.type.localizedCapitalized : actor.role.localizedCapitalized } - static func actorDescription(for actor: ActivityActor) -> String { - if actor.isMCPAgent, let mcpClient = actor.mcpClient, !mcpClient.isEmpty { - return String(format: Self.combinedAgentString, actorRole(for: actor), mcpClient) - } else { - return actorRole(for: actor) - } - } - static func botName(for actor: ActivityActor) -> String? { guard actor.isMCPAgent, let mcpClient = actor.mcpClient, !mcpClient.isEmpty else { return nil diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift index ae20159e5b17..d4eb66fdb027 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowView.swift @@ -61,6 +61,7 @@ struct ActivityLogRowView: View { Text(metadata) .font(.footnote) .foregroundColor(.secondary) + .lineLimit(1) } } } diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift index 01f706db3724..bffa0a4dc249 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift @@ -22,13 +22,8 @@ struct ActivityLogRowViewModel: Identifiable { self.id = activity.activityID if let actor = activity.actor { - actorSubtitle = ActivityStringFormatting.actorRole(for: actor) - if let botName = ActivityStringFormatting.botName(for: actor) { - actorMetadata = botName - } - else { - actorMetadata = nil - } + actorSubtitle = actor.role.isEmpty ? nil : actor.role.localizedCapitalized + actorMetadata = ActivityStringFormatting.botName(for: actor) } else { actorSubtitle = nil actorMetadata = nil From 5ea404ca191d63a7780d1ec24719f1ba0fb8aaab Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:15:22 -0600 Subject: [PATCH 4/4] Add release note for MCP agent info in Activity Log Co-Authored-By: Claude Opus 4.6 --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b90adf672222..1f9b5629c1c8 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -12,6 +12,7 @@ * [*] [internal] Fix retain cycle in Reader Article screen [#25364] * [*] Fix incorrect default Post Format in new posts [#25369] * [*] Posts and Pages are available to XMLRPC-disable sites [#25382] +* [*] Activity Log: Show when actions were performed by an MCP agent [#25397] 26.7 -----