diff --git a/changelog.md b/changelog.md
index cfcdc555b..b549735f8 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,15 @@
# Stream Changelog
+## [Unreleased]
+
+### New Features
+
+- Add AI Client connector for WordPress 7.0+: logs every AI generation call (operation, provider, model, token counts, duration, finish reason, and extended metadata) to Stream when `WP_AI_Client_Event_Dispatcher` is available (XWPENG-20).
+
+### Enhancements
+
+- Show only the first line of multiline summaries in the Stream list table so long entries stay readable in the activity feed.
+
## 4.3.0 - July 18, 2026
### Enhancements
diff --git a/classes/class-connectors.php b/classes/class-connectors.php
index ffeaa75dc..db55d1603 100644
--- a/classes/class-connectors.php
+++ b/classes/class-connectors.php
@@ -71,6 +71,7 @@ public function load_connectors() {
/**
* Core Connectors
*/
+ 'ai-client',
'blogs',
'comments',
'editor',
@@ -325,6 +326,7 @@ protected function build_full_connector_classes() {
// Same canonical slug list load_connectors() uses. Keep in sync.
$connectors = array(
// Core Connectors.
+ 'ai-client',
'blogs',
'comments',
'editor',
diff --git a/classes/class-list-table.php b/classes/class-list-table.php
index 0c2ed5770..b22722b0c 100644
--- a/classes/class-list-table.php
+++ b/classes/class-list-table.php
@@ -317,7 +317,7 @@ public function column_default( $item, $column_name ) {
break;
case 'summary':
- $out = $record->summary;
+ $out = self::format_summary_preview( (string) $record->summary );
$object_title = $record->get_object_title();
/* translators: %s: the title of any object, like a Post (e.g. "Hello World") */
$view_all_text = $object_title ? sprintf( esc_html__( 'View all activity for "%s"', 'stream' ), esc_attr( $object_title ) ) : esc_html__( 'View all activity for this object', 'stream' );
@@ -430,6 +430,30 @@ public function column_default( $item, $column_name ) {
echo wp_kses( $out, $allowed_tags );
}
+ /**
+ * Formats a summary for the admin list table: first line only.
+ *
+ * When the summary contains additional lines, an ellipsis is appended so it
+ * is clear the stored record has more text than the feed preview shows.
+ *
+ * @param string $summary Full summary stored in the database.
+ * @return string Summary preview text (plain text, not HTML).
+ */
+ public static function format_summary_preview( $summary ) {
+ if ( '' === $summary ) {
+ return '';
+ }
+
+ $lines = preg_split( '/\R/u', $summary, 2 );
+ $first = isset( $lines[0] ) ? $lines[0] : $summary;
+
+ if ( ! isset( $lines[1] ) || '' === $lines[1] ) {
+ return $summary;
+ }
+
+ return $first . '…';
+ }
+
/**
* Returns the actions links for the provided record. (Eg. Edit, View)
*
diff --git a/connectors.md b/connectors.md
index 989d38642..53825e1a6 100644
--- a/connectors.md
+++ b/connectors.md
@@ -40,6 +40,28 @@
+## Connector: WP_Stream\Connector_AI_Client
+
+### Actions
+
+ - wp_ai_client_before_generate_result
+ - wp_ai_client_after_generate_result
+
+### Class register()
+
+
+This is the register method for the Connector. Occasionally there are additional actions in here.
+
+```php
+ public function register() {
+ parent::register();
+ add_filter( 'wp_stream_settings_option_fields', array( $this, 'add_settings_fields' ) );
+ add_filter( 'wp_stream_record_array', array( $this, 'filter_wp_stream_record_array' ), 10, 1 );
+ }
+```
+
+
+
## Connector: WP_Stream\Connector_BbPress
### Actions
@@ -289,9 +311,8 @@
```php
public function register() {
parent::register();
- add_action( 'load-theme-editor.php', array( $this, 'get_edition_data' ) );
- add_action( 'load-plugin-editor.php', array( $this, 'get_edition_data' ) );
- add_filter( 'wp_redirect', array( $this, 'log_changes' ) );
+
+ add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'get_edition_data' ), 1 );
}
```
diff --git a/connectors/class-connector-ai-client.php b/connectors/class-connector-ai-client.php
new file mode 100644
index 000000000..0d0fbcdd2
--- /dev/null
+++ b/connectors/class-connector-ai-client.php
@@ -0,0 +1,866 @@
+() method
+ * by replacing non-alphanumeric characters with underscores.
+ *
+ * @var string[]
+ */
+ public $actions = array(
+ 'wp_ai_client_before_generate_result',
+ 'wp_ai_client_after_generate_result',
+ );
+
+ /**
+ * In-flight generations keyed by the model object itself.
+ *
+ * WP AI Client fires before/after hooks with the *same* model instance and does
+ * not expose a request ID. SplObjectStorage uses object identity (O(1)), keeps
+ * the model alive so PHP cannot recycle spl_object_id, and avoids pairing a
+ * later generation with a stale pending row when the after-hook never fires.
+ *
+ * @var \SplObjectStorage