-
Notifications
You must be signed in to change notification settings - Fork 33
Add Type Ahead experiment #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Adds AI-powered autocomplete suggestions: - Real-time text completion in the block editor - Context-aware suggestions based on content - Keyboard shortcuts for accepting suggestions
The generate_suggestion() method was calling a non-existent get_model_preferences() method. Changed to use the shared get_preferred_models() helper function like other abilities.
- Prioritize gpt-5.1-nano, then claude-haiku-4-5 for faster responses - Lower ghost text z-index to avoid overlap with WordPress UI elements
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #151 +/- ##
==============================================
- Coverage 46.89% 35.60% -11.30%
- Complexity 208 263 +55
==============================================
Files 19 22 +3
Lines 1271 1691 +420
==============================================
+ Hits 596 602 +6
- Misses 675 1089 +414
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds an AI-powered type-ahead experiment that provides inline ghost text autocomplete suggestions in the WordPress block editor. Users can accept suggestions using keyboard shortcuts (Tab for full, Ctrl+Right for word/sentence, Escape to dismiss). The feature includes configurable completion modes and targets fast, low-cost AI models for responsive performance.
Key changes:
- New Type Ahead ability with structured input/output schemas and caching
- React-based block editor integration with ghost text overlay and keyboard handling
- Experiment registration with comprehensive settings UI (mode, delay, confidence, max words)
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| webpack.config.js | Adds entry point for the type-ahead experiment bundle |
| src/utils/run-ability.ts | New utility for safe ability execution with REST API fallback |
| src/experiments/type-ahead/style.scss | Styles for ghost text overlay positioning and appearance |
| src/experiments/type-ahead/index.tsx | Main React component with DOM tracking, caret monitoring, and keyboard handlers |
| includes/Experiments/Type_Ahead/Type_Ahead.php | Experiment registration, settings, and asset enqueuing |
| includes/Experiment_Loader.php | Registers Type_Ahead experiment in default experiments list |
| includes/Abilities/Type_Ahead/system-instruction.php | AI prompt instruction for generating inline completions |
| includes/Abilities/Type_Ahead/Type_Ahead.php | Ability implementation with AI client integration and caching |
| docs/experiments/type-ahead.md | Documentation for hooks, data flow, and testing procedures |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
|
|
||
| lookup(); | ||
| const interval = window.setInterval( lookup, 750 ); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This interval-based polling (every 750ms) to find block DOM elements is inefficient and could impact performance, especially with many blocks. Consider using a MutationObserver to detect when the block element is added to the DOM instead of continuous polling.
| if ( mb_strlen( $value ) > self::CONTEXT_LIMIT ) { | ||
| return mb_substr( $value, -1 * self::CONTEXT_LIMIT ); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The truncation takes the last CONTEXT_LIMIT characters using a negative offset. For multi-byte UTF-8 characters, this could potentially cut in the middle of a character sequence. While 'mb_substr' should handle this correctly, verify that 'normalize_content' doesn't introduce any issues with multi-byte characters at boundaries.
| if ( mb_strlen( $value ) > self::CONTEXT_LIMIT ) { | |
| return mb_substr( $value, -1 * self::CONTEXT_LIMIT ); | |
| $length = mb_strlen( $value, 'UTF-8' ); | |
| if ( $length > self::CONTEXT_LIMIT ) { | |
| $start = $length - self::CONTEXT_LIMIT; | |
| return mb_substr( $value, $start, null, 'UTF-8' ); |
| let hasShownFallbackNotice = false; | ||
|
|
||
| const getAbilityClient = () => | ||
| ( window as Record< string, any > )?.wp?.abilities ?? null; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type casting uses 'as Record<string, any>' and 'as any' which bypasses TypeScript's type safety. Consider defining a proper interface for the window.wp.abilities object to maintain type safety throughout the codebase.
| : addQueryArgs( | ||
| `/wp-abilities/v1/abilities/${ ability }/run`, | ||
| { | ||
| input: normalizedInput, | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When using 'GET' or 'DELETE' methods with input, the input is added as query parameters via 'addQueryArgs'. However, complex objects or arrays in 'input' might not serialize correctly as query strings. Consider validating that input is serializable for GET/DELETE requests, or document that these methods only support simple scalar values.
| if ( str_starts_with( $clean, '```' ) ) { | ||
| $clean = preg_replace( '/^```[a-zA-Z0-9_-]*\s*/', '', $clean ) ?? $clean; | ||
| if ( str_contains( $clean, '```' ) ) { |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function uses 'str_starts_with' and 'str_contains', which require PHP 8.0+. However, there's no minimum PHP version check visible in this file. If the codebase supports PHP 7.x, this will cause fatal errors. Either add a PHP version check in the file or ensure polyfills are in place.
| if ( | ||
| ( event.metaKey || event.ctrlKey ) && | ||
| event.code === 'Space' | ||
| ) { | ||
| event.preventDefault(); | ||
| setRequestNonce( ( prev ) => prev + 1 ); | ||
| scheduleFetch( true ); | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The keyboard shortcut Ctrl+Space (lines 762-769) may conflict with system-level keyboard shortcuts. On macOS, Cmd+Space is typically used for Spotlight search, and on some Linux systems Ctrl+Space is used for input method switching. While the event.preventDefault() will stop the default, document this potential conflict and consider providing an alternative shortcut or making it configurable.
| const textNode = doc.createTextNode( text ); | ||
| range.insertNode( textNode ); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'createTextNode' and 'insertNode' operations use unsanitized text from the suggestion. While the suggestion goes through 'sanitize_textarea_field' on the server side (line 366), ensure the text returned from the API hasn't been tampered with during transport. Consider additional client-side sanitization before DOM insertion to prevent potential XSS if the API response is compromised.
| $clean = trim( $raw ); | ||
|
|
||
| if ( str_starts_with( $clean, '```' ) ) { | ||
| $clean = preg_replace( '/^```[a-zA-Z0-9_-]*\s*/', '', $clean ) ?? $clean; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern '/^[a-zA-Z0-9_-]*\s*/' allows matching code blocks, but the hyphen in the character class should be escaped or placed at the start/end to avoid being interpreted as a range. While this works because '_-' is a valid range in ASCII, it's better practice to write it as '/^[a-zA-Z0-9_\-]\s/' or '/^```[a-zA-Z0-9-_]\s/' for clarity.
| $clean = preg_replace( '/^```[a-zA-Z0-9_-]*\s*/', '', $clean ) ?? $clean; | |
| $clean = preg_replace( '/^```[a-zA-Z0-9_\\-]*\s*/', '', $clean ) ?? $clean; |
|
|
||
| Asset_Loader::localize_script( | ||
| 'type_ahead', | ||
| 'TypeAheadData', |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The localized script data uses 'TypeAheadData' as the object name, but the TypeScript code expects 'aiTypeAheadData' (line 828 in index.tsx checks 'window.aiTypeAheadData'). This mismatch will cause the feature to fail silently after the 5-second polling timeout. The object name should be 'aiTypeAheadData' to match the frontend expectation.
| 'TypeAheadData', | |
| 'aiTypeAheadData', |
| private function build_cache_key( string $block_content, string $preceding_text, string $mode, int $max_words ): string { | ||
| return 'type_ahead_' . md5( $block_content . '|' . $preceding_text . '|' . $mode . '|' . $max_words ); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cache key includes 'preceding_text' which will cause cache misses for identical content at different cursor positions. This defeats the purpose of caching when users move the cursor backward and forward in the same text. Consider whether cursor position should actually be part of the cache key, or if a more semantic key would be more effective.
|
@Jameswlepage can you add tests here like was done in https://github.com/WordPress/ai/pull/147/changes#diff-0cf348eda48cae411ad9d44a2e5b449603a971f2313b0a849893ff4cf8280c46? |
|
Summary
Model preferences
Prioritizes fast, low-cost models for responsive completions:
gpt-5.1-nano→claude-haiku-4-5→gemini-2.5-flash→gpt-4o-miniCost estimate:
$0.004 per 5-minute session with GPT-5 Nano ($0.05/hour). Falls back to Claude Haiku 4.5 if unavailable (~$1/hour).Changes
includes/Abilities/Type_Ahead/- Ability class for generating suggestionsincludes/Experiments/Type_Ahead/- Experiment registration with settings UIsrc/experiments/type-ahead/- Block editor integration with ghost text overlaysrc/utils/run-ability.ts- Utility for invoking abilities from JavaScriptTest plan