Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/experiments/alt-text-generation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import { createHigherOrderComponent } from '@wordpress/compose';
*/
import { AltTextControls } from './components/AltTextControls';
import type { ImageBlockAttributes } from './types';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Alt Text Generation',
description: 'Generates descriptive alt text for image blocks using AI.',
abilitySlug: 'ai/alt-text-generation',
} );

interface BlockEditProps {
clientId: string;
Expand Down
7 changes: 7 additions & 0 deletions src/experiments/comment-moderation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import { createRoot } from '@wordpress/element';
* Internal dependencies
*/
import { LazyAnalysisController } from './components/LazyAnalysisController';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Comment Moderation',
description: 'Analyses pending comments for spam and quality using AI.',
abilitySlug: 'ai/comment-analysis',
} );

/**
* Initialize the comment moderation experiment.
Expand Down
7 changes: 7 additions & 0 deletions src/experiments/content-classification/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ import { addFilter } from '@wordpress/hooks';
* Internal dependencies
*/
import SuggestionPanel from './components/SuggestionPanel';
import { exposeToDevTools } from '../../utils/devtools';

/**
* Styles
*/
import './index.scss';

exposeToDevTools( {
name: 'Content Classification',
description: 'Suggests tags and categories for the current post using AI.',
abilitySlug: 'ai/content-classification',
} );

const SUPPORTED_TAXONOMIES = [ 'post_tag', 'category' ];

/**
Expand Down
7 changes: 7 additions & 0 deletions src/experiments/content-resizing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import { addFilter } from '@wordpress/hooks';
*/
import ContentResizingToolbar from './components/ContentResizingToolbar';
import './index.scss';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Content Resizing',
description: 'Expands or condenses selected paragraph content using AI.',
abilitySlug: 'ai/content-resizing',
} );

const { aiContentResizingData } = window as any;

Expand Down
7 changes: 7 additions & 0 deletions src/experiments/editorial-notes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import { registerPlugin } from '@wordpress/plugins';
* Internal dependencies
*/
import EditorialNotesPlugin from './components/EditorialNotesPlugin';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Editorial Notes',
description: 'Reviews the post and provides editorial feedback using AI.',
abilitySlug: 'ai/editorial-notes',
} );

registerPlugin( 'ai-editorial-notes', {
render: () => (
Expand Down
8 changes: 8 additions & 0 deletions src/experiments/editorial-updates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import { registerPlugin } from '@wordpress/plugins';
* Internal dependencies
*/
import EditorialUpdatesPlugin from './components/EditorialUpdatesPlugin';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Editorial Updates',
description:
'Rewrites selected post content based on editorial instructions using AI.',
abilitySlug: 'ai/editorial-updates',
} );

if ( ( window as any ).aiEditorialUpdatesData?.enabled ) {
registerPlugin( 'ai-editorial-updates', {
Expand Down
7 changes: 7 additions & 0 deletions src/experiments/excerpt-generation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import { registerPlugin } from '@wordpress/plugins';
*/
import ExcerptGeneration from './components/ExcerptGeneration';
import ExcerptInlineWrapper from './components/ExcerptInlineWrapper';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Excerpt Generation',
description: 'Generates a post excerpt from the post body using AI.',
abilitySlug: 'ai/excerpt-generation',
} );

/**
* Plugin component that adds a generate button to the excerpt panel.
Expand Down
7 changes: 7 additions & 0 deletions src/experiments/meta-description/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import { __ } from '@wordpress/i18n';
*/
import MetaDescriptionPanel from './components/MetaDescriptionPanel';
import './index.scss';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Meta Description',
description: 'Generates an SEO meta description for the post using AI.',
abilitySlug: 'ai/meta-description',
} );

import type { MetaDescriptionData } from './types';

Expand Down
7 changes: 7 additions & 0 deletions src/experiments/summarization/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { registerPlugin } from '@wordpress/plugins';
import SummarizationPlugin from './components/SummarizationPlugin';
import SummarizationBlockControls from './components/SummarizationBlockControls';
import './index.scss';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Summarization',
description: 'Generates a summary of the current post content using AI.',
abilitySlug: 'ai/summarization',
} );

// Register the plugin.
registerPlugin( 'classifai-plugin-summarization', {
Expand Down
7 changes: 7 additions & 0 deletions src/experiments/title-generation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { registerPlugin } from '@wordpress/plugins';
import './index.scss';
import TitleToolbar from './components/TitleToolbar';
import { TitleToolbarWrapper } from './components/TitleToolbarWrapper';
import { exposeToDevTools } from '../../utils/devtools';

exposeToDevTools( {
name: 'Title Generation',
description: 'Generates a post title from the post content using AI.',
abilitySlug: 'ai/title-generation',
} );

// For template preview mode (when title is a block)
// Use filter to add toolbar to post-title block
Expand Down
8 changes: 8 additions & 0 deletions src/features/image-generation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
/**
* Internal dependencies
*/
import { exposeToDevTools } from '../../utils/devtools';
import './featured-image';
import './media-library';
import './media-library-editor';
import './inline';

exposeToDevTools( {
name: 'Image Generation',
description:
'Generates images from text prompts and inserts them into posts, media library, or as featured images.',
abilitySlug: 'ai/image-generation',
} );
37 changes: 37 additions & 0 deletions src/utils/devtools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Chrome DevTools 3P Tools bootstrap.
*
* Listens for the devtoolstooldiscovery event and registers the AI plugin
* ToolGroup. A window-level guard ensures the listener is registered exactly
* once even when multiple feature bundles load on the same page.
*
* @see https://developer.chrome.com/blog/devtools-for-agents-3p-tools
*/

/**
* Internal dependencies
*/
import { getToolGroup } from './tools';

export { exposeToDevTools } from './registry';

// Extend Window to declare the bootstrap flag and the Chrome DevTools MCP runtime.
declare global {
interface Window {
__aiDevToolsBootstrapped?: boolean;
}
}

// The devtoolstooldiscovery event carries a respondWith method.
interface DevToolsToolDiscoveryEvent extends Event {
respondWith: ( toolGroup: ReturnType< typeof getToolGroup > ) => void;
}

if ( ! window.__aiDevToolsBootstrapped ) {
window.__aiDevToolsBootstrapped = true;

window.addEventListener( 'devtoolstooldiscovery', ( event: Event ) => {
const discoveryEvent = event as DevToolsToolDiscoveryEvent;
discoveryEvent.respondWith( getToolGroup() );
} );
}
57 changes: 57 additions & 0 deletions src/utils/devtools/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Feature capability registry for Chrome DevTools 3P Tools.
*
* Uses window.__aiFeatures as the backing store so registrations from
* multiple separate webpack bundles are all visible to the single DevTools
* listener that is registered by whichever bundle loads first.
*/

export interface DevToolsRegistration {
name: string;
description: string;
abilitySlug: string;
}

declare global {
interface Window {
__aiFeatures?: DevToolsRegistration[];
}
}

/**
* Returns the store of registered features.
*
* @since x.x.x
* @return {DevToolsRegistration[]} List of registered feature registrations.
*/
function getStore(): DevToolsRegistration[] {
if ( ! window.__aiFeatures ) {
window.__aiFeatures = [];
}
return window.__aiFeatures;
}

/**
* Registers an AI feature so it appears in the get_ai_capabilities DevTools tool.
*
* Call this once from the feature's entry point at module level.
*
* @since x.x.x
* @param {DevToolsRegistration} registration The feature metadata.
*/
export function exposeToDevTools( registration: DevToolsRegistration ): void {
const store = getStore();
if ( ! store.find( ( e ) => e.abilitySlug === registration.abilitySlug ) ) {
store.push( registration );
}
}

/**
* Returns all registered features.
*
* @since x.x.x
* @return {DevToolsRegistration[]} List of registered AI feature registrations.
*/
export function getDevToolsRegistrations(): DevToolsRegistration[] {
return getStore();
}
133 changes: 133 additions & 0 deletions src/utils/devtools/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* In-memory ring buffer of AI request records for Chrome DevTools 3P Tools.
*
* Uses window.__aiRequestLog as the backing store so records written by any
* feature bundle are visible to the single DevTools listener registered by
* whichever bundle loads first.
*/

export interface AIRequestRecord {
id: string;
ability: string;
input: unknown;
output: unknown;
status: 'pending' | 'success' | 'error';
startedAt: string;
durationMs: number | null;
error: string | null;
}

declare global {
interface Window {
__aiRequestLog?: AIRequestRecord[];
__aiRequestLogNextId?: number;
}
}

const MAX_RECORDS = 50;

/**
* Returns the store of request records.
*
* @since x.x.x
* @return {AIRequestRecord[]} List of request records.
*/
function getRecords(): AIRequestRecord[] {
if ( ! window.__aiRequestLog ) {
window.__aiRequestLog = [];
}
return window.__aiRequestLog;
}

/**
* Returns the next request record ID.
*
* @since x.x.x
* @return {string} The next request record ID.
*/
function nextId(): string {
if ( ! window.__aiRequestLogNextId ) {
window.__aiRequestLogNextId = 1;
}
return String( window.__aiRequestLogNextId++ );
}

/**
* Records the start of an ability execution.
*
* @since x.x.x
* @param {string} ability The ability slug being executed.
* @param {unknown} input The input passed to the ability.
* @return {string} The record ID, used to complete or error the record.
*/
export function recordStart( ability: string, input: unknown ): string {
const id = nextId();

const record: AIRequestRecord = {
id,
ability,
input,
output: null,
status: 'pending',
startedAt: new Date().toISOString(),
durationMs: null,
error: null,
};

const log = getRecords();
log.push( record );

if ( log.length > MAX_RECORDS ) {
log.shift();
}

return id;
}

/**
* Marks a pending record as successfully completed.
*
* @since x.x.x
* @param {string} id The record ID returned by recordStart.
* @param {unknown} output The ability response.
*/
export function recordComplete( id: string, output: unknown ): void {
const record = getRecords().find( ( r: AIRequestRecord ) => r.id === id );
if ( ! record ) {
return;
}
record.output = output;
record.status = 'success';
record.durationMs =
new Date().getTime() - new Date( record.startedAt ).getTime();
}

/**
* Marks a pending record as failed.
*
* @since x.x.x
* @param {string} id The record ID returned by recordStart.
* @param {string} error The error message.
*/
export function recordError( id: string, error: string ): void {
const record = getRecords().find( ( r: AIRequestRecord ) => r.id === id );
if ( ! record ) {
return;
}
record.status = 'error';
record.error = error;
record.durationMs =
new Date().getTime() - new Date( record.startedAt ).getTime();
}

/**
* Returns recent records in reverse-chronological order.
*
* @since x.x.x
* @param {number} limit Maximum number of records to return (1–50).
* @return {AIRequestRecord[]} List of AI request records.
*/
export function getHistory( limit: number = 10 ): AIRequestRecord[] {
const clamped = Math.min( Math.max( limit, 1 ), MAX_RECORDS );
return getRecords().slice( -clamped ).reverse();
}
Loading
Loading