3 Commits

Author SHA1 Message Date
Dwindi Ramadhana
d3f142222c feat: Add connection test caching and reasoning content support
Backend improvements:
- Add cache auto-clear on settings save (class-settings-v2.php)
  - Hooks updated_option action
  - Clears connection test transients when local backend settings change
- Add reasoning_content streaming support (class-local-backend-provider.php)
  - Handles thinking models like Claude extended thinking
  - Captures chunk['choices'][0]['delta']['reasoning_content']

Documentation:
- Add FRONTEND_AND_CHAT_FIX_SUMMARY.md with all fixes
- Add FRONTEND-REFACTOR-PHASE2.md with modularization plan

Note: Splitting effort deferred - will continue iterating on monolith
Next: Fix session history not appearing bug
2026-06-17 05:26:12 +07:00
Dwindi Ramadhana
f55acd7d26 Add sidebar 1:1 migration docs 2026-06-15 11:47:03 +07:00
Dwindi Ramadhana
619d36d3c8 feat: MEMANTO integration — persistent memory for cross-session context (Phases 1-4)
Phase 1: Core Client
- New class-memanto-client.php: Singleton PHP client for MEMANTO API v2
  - Health check with 5-min transient caching
  - Agent CRUD (ensure, activate, deactivate sessions)
  - Memory operations (remember, batch_remember, recall, recall_recent)
  - Auto re-activation on expired session tokens (401 retry)

Phase 2: Write-Through Memory Hooks
- New class-memanto-context-enhancer.php: Orchestrates remember/recall
  - Fires on: user message, plan generated/approved/rejected,
    section written, block refined, config saved, session start/end
  - All hooks via do_action() — zero coupling to MEMANTO when disabled

Phase 3: Context Enrichment
- Context builder injects recalled memories into AI prompts
  via build_memanto_context() in build_working_context()
- 3-recall strategy: recent post memories, semantic search, user preferences
- Deduplication by content hash

Phase 4: Cross-Session Restore
- New REST endpoints: /memanto/restore, /memanto/preferences
- restore_session() recalls 15 recent memories + user preferences on editor load
- build_session_restore_message() creates AI-ready system message
- get_user_preferences_for_new_post() extracts tone/audience/length/language
- Frontend: 🧠 Restored badge in status bar with memory count tooltip
- Preference carry-over: auto-fills post config from stored user preferences
- deactivate_session() called on session end (triggers MEMANTO summary)
- Badge clears on new conversation start

Settings UI:
- MEMANTO Context Keeper section with enable toggle, URL, API key, test connection
- Settings registered via class-settings-v2.php + tab-memanto.php view

Graceful degradation: all MEMANTO calls guarded by is_active(),
frontend catches silently, plugin works identically when disabled.
2026-06-08 12:42:04 +07:00
17 changed files with 30943 additions and 19112 deletions

302
FRONTEND-REFACTOR-PHASE2.md Normal file
View File

@@ -0,0 +1,302 @@
# Frontend Refactor Phase 2: Modularization Plan
**Date**: 2026-06-17
**Status**: 📋 IN PROGRESS
**Author**: Agent
**Reference**: `FRONTEND_AND_CHAT_FIX_SUMMARY.md`
---
## Session Summary (2026-06-17)
### Completed Backend Improvements
| Task | File | Status |
|------|------|--------|
| Connection test caching | `class-provider-manager.php` | ✅ Already existed |
| Cache auto-clear on settings save | `class-settings-v2.php` | ✅ Added |
| Reasoning content parsing | `class-local-backend-provider.php` | ✅ Added |
### Files Modified This Session
1. **`includes/class-settings-v2.php`**
- Added `clear_local_backend_cache_on_settings_change()` method
- Hooked to `updated_option` action
- Clears connection test transients when local backend settings change
2. **`includes/class-local-backend-provider.php`**
- Added `reasoning_content` streaming support (lines ~494-520)
- Handles thinking models like Claude extended thinking
- Debug logging included
### Validation Results
| Check | Result |
|-------|--------|
| `npm run build` | ✅ Passes |
| PHP syntax (all modified files) | ✅ Passes |
| Build output | `dist/sidebar.js` (169 KB) |
---
## Overview
This document outlines the approach for splitting the monolithic `assets/js/src/index.jsx` (11,793 lines) into modular React components, hooks, and utilities while maintaining the existing build pipeline.
### Key Principle
> The build pipeline (`scripts/build.js` → `assets/js/dist/sidebar.js`) **already works correctly**. We are refactoring the source for maintainability, NOT fixing a broken build.
### Important: Two sidebar.js Files
| File | Source of Truth? | Loaded by PHP? |
|------|------------------|---------------|
| `assets/js/sidebar.js` | ❌ Legacy (webpack, 438KB) | ❌ No |
| `assets/js/dist/sidebar.js` | ✅ Current (esbuild, 169KB) | ✅ Yes |
> **Do NOT confuse the two files.** The legacy `sidebar.js` (438KB) is the original monolithic file that was never modularized. The `dist/sidebar.js` (169KB) is the esbuild-compiled output from `src/index.jsx`.
---
## Current State
### Build Pipeline (Working ✅)
```
assets/js/src/index.jsx
│ esbuild (bundle: true)
│ format: "iife"
│ globalName: undefined (anonymous IIFE)
assets/js/dist/sidebar.js (compiled, served to WordPress) ✅
```
### Source & Output Files
| Path | Lines/Size | Purpose | Status |
|------|------------|---------|--------|
| `src/index.jsx` | 11,793 | React source (to be split) | ✅ Active |
| `dist/sidebar.js` | 169 KB | Compiled output (loaded by PHP) | ✅ Active |
| `sidebar.js` | 438 KB | **Legacy file** (NOT loaded) | ⚠️ Archived |
### Build Validation (2026-06-17) ✅
| Check | Result |
|-------|--------|
| `npm run build` | ✅ Passes (165.8kb output) |
| PHP syntax (settings) | ✅ Passes |
| PHP syntax (provider) | ✅ Passes |
| PHP syntax (provider manager) | ✅ Passes |
| PHP syntax (local backend) | ✅ Passes |
| PHP loads `dist/sidebar.js` | ✅ Confirmed in `class-gutenberg-sidebar.php:217` |
### esbuild Configuration
The `scripts/build.js` uses `bundle: true`, which means:
- All local imports are resolved automatically
- No need for separate bundler config
- Extracted files will compile seamlessly
---
## Proposed Directory Structure
```
assets/js/
├── src/
│ ├── index.jsx # Main entry, imports all modules
│ ├── components/
│ │ ├── ChatTab.jsx # Chat tab content
│ │ ├── ConfigTab.jsx # Configuration tab
│ │ ├── CostTab.jsx # Cost tracking tab
│ │ ├── WelcomeScreen.jsx # Welcome/home screen
│ │ ├── Clarification.jsx # Clarification quiz UI
│ │ ├── AgentWorkspaceCard.jsx
│ │ ├── ContextualAction.jsx
│ │ ├── FocusKeywordBar.jsx
│ │ ├── Messages.jsx # Message rendering
│ │ ├── GlobalStatusBar.jsx
│ │ └── RefineAllModal.jsx
│ ├── hooks/
│ │ ├── useChatHistory.js
│ │ ├── useSessionLock.js
│ │ ├── usePostConfig.js
│ │ ├── useWritingState.js
│ │ └── useStreaming.js
│ ├── utils/
│ │ ├── api.js # REST API helpers
│ │ ├── blockUtils.js # Block manipulation
│ │ ├── planUtils.js # Plan parsing/building
│ │ ├── streamUtils.js # Streaming utilities
│ │ ├── markdownUtils.js # Markdown rendering
│ │ └── formatUtils.js # Formatting helpers
│ └── styles/
│ └── components.css # Component-specific styles
└── dist/
└── sidebar.js # Compiled output (auto-generated)
```
---
## Extraction Strategy
### Phase 1: Extract Utilities (Low Risk)
Begin with pure functions that have no React dependencies:
1. **`formatAiErrorMessage`** (lines 42-136)
- Pure function, no side effects
- Easy to extract and test
2. **`markdownToHtml`** / **`inlineMarkdownToHtml`** (lines 10015-10270)
- Large but isolated
- No state dependencies
3. **Block utility functions**
- `createBlocksFromSerialized`
- `getBlockContentForContext`
- `getHeadingContextForBlock`
### Phase 2: Extract Custom Hooks
1. **`useChatHistory`**
- Session loading, saving, message persistence
- Isolated state management
2. **`useSessionLock`**
- Tab locking mechanism
- Heartbeat management
3. **`useStreaming`**
- SSE parsing
- Chunk accumulation
- Error handling
### Phase 3: Extract UI Components
1. **Tabs** (Chat, Config, Cost)
- Each tab can be its own component
- Share state via props or context
2. **WelcomeScreen**
- Self-contained, minimal dependencies
3. **Clarification Quiz**
- Complex but isolated UI
### Phase 4: Extract Editor Integration
The most complex part - interactions with WordPress block editor:
- Block mutation observation
- Input blocking
- Undo/redo integration
---
## Migration Pattern
### Before (in index.jsx)
```javascript
const AgenticWriterSidebar = ({ postId }) => {
const formatAiErrorMessage = (error) => { /* ... */ };
const sendMessage = async (msg) => { /* ... */ };
return <div>...</div>;
};
```
### After (modular)
**`src/utils/formatUtils.js`**
```javascript
export const formatAiErrorMessage = (error, fallback, settings) => {
// ...
};
```
**`src/hooks/useChatApi.js`**
```javascript
export const useChatApi = () => {
const sendMessage = async (msg) => { /* ... */ };
return { sendMessage };
};
```
**`src/index.jsx`**
```javascript
import { formatAiErrorMessage } from './utils/formatUtils';
import { useChatApi } from './hooks/useChatApi';
import { ChatTab } from './components/ChatTab';
const AgenticWriterSidebar = ({ postId }) => {
const { sendMessage } = useChatApi();
return <ChatTab onSend={sendMessage} />;
};
```
---
## Verification Checklist
After each extraction:
- [ ] `npm run build` completes without errors
- [ ] Compiled `sidebar.js` matches expected size (±5%)
- [ ] Chat functionality works (send message, receive response)
- [ ] Planning functionality works (generate plan)
- [ ] Refinement works (refine blocks)
- [ ] Tab switching works
- [ ] Session persistence works
- [ ] No console errors in browser
---
## Rollback Plan
If extraction causes issues:
1. Revert the specific extracted file
2. Keep extracted utilities (safe to keep)
3. Re-run `npm run build`
4. Verify functionality
---
## Next Action
1. **Create `src/utils/` directory**
2. **Extract `formatAiErrorMessage`** as the first migration
3. **Verify build and functionality**
4. **Iterate with next extraction**
---
## Dependencies
| Tool | Status |
|------|--------|
| esbuild | ✅ Configured |
| npm | ✅ Available |
| WordPress environment | ✅ Local by Flywheel |
---
## Questions to Resolve Before Starting
1. Should extracted components use TypeScript or remain JSX?
2. Should we add PropTypes for component prop validation?
3. Should we maintain backward compatibility with `wpAgenticWriter` global?
---
## References
- `FRONTEND_AND_CHAT_FIX_SUMMARY.md` - Original fix documentation
- `scripts/build.js` - Current esbuild configuration
- `assets/js/dist/sidebar.js` - **Compiled output** (loaded by PHP, 169KB)
- `assets/js/sidebar.js` - **Legacy file** (archived, 438KB, NOT loaded)
- `assets/js/src/index.jsx` - Current source (to be split, 11,793 lines)

View File

@@ -0,0 +1,122 @@
# WP Agentic Writer Fix Summary: Chat & Frontend Build
**Date**: 2026-06-16 (original) / 2026-06-17 (updates)
**Status**: 🟢 RESOLVED
This document serves as a hands-off record of the debugging steps, root causes, and fixes applied to resolve the "empty chat response" issue and establish a proper frontend build pipeline.
---
## Updates (2026-06-17)
### Additional Backend Improvements
1. **Connection Test Caching** - Already existed in `class-provider-manager.php` (5-min TTL via transients)
2. **Cache Auto-Clear on Settings Save** - Added to `class-settings-v2.php`
- Hooks to `updated_option` action
- Clears connection test cache when local backend settings change
3. **Reasoning Content Parsing** - Added to `class-local-backend-provider.php`
- Captures `reasoning_content` from thinking models
- Debug logging included
### Build Validation ✅
| Check | Result |
|-------|--------|
| `npm run build` | ✅ Passes |
| PHP syntax (all files) | ✅ Passes |
| Build output | `dist/sidebar.js` (169 KB) |
### Important: Two sidebar.js Files
| File | Loaded by PHP? |
|------|----------------|
| `assets/js/sidebar.js` (438 KB) | ❌ Legacy, NOT loaded |
| `assets/js/dist/sidebar.js` (169 KB) | ✅ Current, loaded |
---
## Original Fixes (2026-06-16)
---
## 1. Frontend Build Pipeline Established
**Problem**:
The PHP plugin explicitly loads `assets/js/dist/sidebar.js`. However, ongoing refactoring work was occurring in `assets/js/src/index.jsx`. Because there was no active build process, changes made in `index.jsx` were not reflecting in the application.
**Solution**:
* Created `package.json` with `esbuild` to handle fast JSX compilation.
* Added `scripts/build.js` configured specifically for WordPress Gutenberg (using `wp.element.createElement` and `wp.element.Fragment`).
* **Critical Fix**: Ensured the esbuild configuration wraps the output in an anonymous IIFE (`format: "iife"`) *without* defining a `globalName: "wp"`. A named global would have caused the compiled script to overwrite WordPress's native `window.wp` object with `undefined`, breaking the editor.
**Usage**:
* Run `npm run build` to compile `src/index.jsx` -> `dist/sidebar.js`.
* Run `npm run build:watch` during active development.
---
## 2. Backend Fix: PHP Fatal Error on Connection Test
**Problem**:
When the local backend provider received an HTTP 4xx/5xx error (e.g., when trying to test a connection to a rate-limited or failing model), the entire REST request crashed, resulting in a 500 server error and breaking the chat flow entirely.
**Root Cause**:
In `includes/class-local-backend-provider.php` (line 882), a double-quoted string was used for a `sprintf` format:
`__("API Error (HTTP %1$d): %2$s", "wp-agentic-writer")`
Because it was double-quoted, PHP attempted to interpolate `$d` and `$s` as variables, resolving them to empty strings. The resulting string `API Error (HTTP %1): %2` caused `sprintf` to throw a fatal `ValueError: Unknown format specifier ")"`.
**Fix**:
Changed the double quotes to single quotes to prevent PHP variable interpolation, preserving the intended positional specifiers:
`__('API Error (HTTP %1$d): %2$s', "wp-agentic-writer")`
*Scanned the rest of the codebase and confirmed this dangerous pattern does not exist anywhere else.*
---
## 3. Backend Fix: Agentic Models Returning "Empty Responses"
**Problem**:
The user experienced intermittent "empty chat response" errors even when the API connection was successful (HTTP 200).
**Root Cause**:
The configured models (e.g., `dough/kr/claude-sonnet-4.5-thinking` and `ag/gemini-3-flash-agent`) are **agentic/tool-calling** models. When fed a large WordPress system prompt asking for plain prose, these models burned all their tokens on internal "reasoning", attempted to emit a function/tool call, failed (`finish_reason: "malformed_function_call"`), and returned **zero text content**.
Because the streaming buffer received no content, it fell back to non-streaming, which also yielded zero content, triggering a generic "empty response" error.
**Fix**:
1. **Better Error Surfacing**: Updated the streaming and fallback logic in `class-local-backend-provider.php` to actively capture the `finish_reason` payload.
2. If the provider returns empty content but has a finish reason of `malformed_function_call`, `tool_calls`, or `function_call`, the backend now intercepts this and throws a highly specific, actionable error:
> *"The selected model [Model Name] returned no text (finish reason: tool/function call). This usually means an agentic/coding model is being used for prose. Choose a standard chat model (without an -agent or -agentic suffix) in Settings."*
3. **Config Alignment**: Used WP-CLI to update the WordPress options table, switching the local backend models from the agentic variants to the standard `gemini/gemini-3-flash-preview` for all prose tasks (`chat`, `writing`, `planning`, etc.).
---
## Summary of Touched Files
* `package.json` (New)
* `scripts/build.js` (New)
* `assets/js/dist/sidebar.js` (Rebuilt)
* `includes/class-local-backend-provider.php` (Fixed string interpolation, added `finish_reason` edge-case handling, added error payload parsing).
The chat functionality is now robust, gracefully handles agentic model failures, and the React frontend can be reliably compiled from `index.jsx`.
---
## 4. Next Recommendations
With the frontend build pipeline established and the critical backend crashes resolved, the foundation is stable. Here are the recommended next steps:
1. **Complete the React File Splitting (Refactor Phase 2)** ⚠️ IN PROGRESS
* Now that `index.jsx` compiles successfully to `dist/sidebar.js`, the massive 11,000+ line `index.jsx` should be split into modular React components.
* **See `FRONTEND-REFACTOR-PHASE2.md` for the detailed modularization plan.**
* The new `esbuild` setup natively supports resolving local imports, meaning you can safely extract components into a `src/components/` directory and import them into `index.jsx` without changing the PHP backend.
* **Key clarification**: The source is `src/index.jsx`, which compiles to `dist/sidebar.js`. We are NOT creating a new `sidebar.jsx` source file - we are modularizing the existing `index.jsx`.
2. **Optimize `test_connection()` in the Provider Manager** ✅ COMPLETED
* Implemented connection test caching using WordPress transients with 5-minute TTL in `class-provider-manager.php`.
* Added automatic cache clearing in `class-settings-v2.php` when local backend settings are saved (URL, API key, model changes).
* This eliminates redundant connection tests on every chat request, significantly reducing latency.
3. **Handle Reasoning Tokens for Thinking Models** ✅ COMPLETED
* Added `reasoning_content` parsing to `chat_stream()` method in `class-local-backend-provider.php`.
* The streaming parser now captures `chunk["choices"][0]["delta"]["reasoning_content"]` from thinking models like Claude extended thinking.
* Reasoning content is passed through the callback so the frontend can optionally display it in a collapsible section.
* Debug logging added to help identify when reasoning chunks are received.

768
SIDEBAR_1_TO_1_MIGRATION.md Normal file
View File

@@ -0,0 +1,768 @@
# WP Agentic Writer Sidebar 1:1 Migration Plan
**Source of truth:** `assets/js/sidebar.js`
**Source length:** 12,363 lines
**Scope:** planning/audit document only. No implementation code, no build step, no inferred behavior.
This document exists to make the migration match `sidebar.js` exactly. If this file, `MIGRATION_GUIDE.md`, comments, or memory disagree with `assets/js/sidebar.js`, `assets/js/sidebar.js` wins.
## Non-Negotiable Rules
1. Preserve every behavior from `assets/js/sidebar.js` before extracting or improving anything.
2. Convert `wp.element.createElement(...)` to JSX mechanically only after copying the same source range.
3. Preserve hook order, state names, ref names, default values, dependency arrays, effect cleanups, and callback boundaries.
4. Preserve all `wpAgenticWriter` globals, endpoint paths, request bodies, headers, nonces, stream handling, abort handling, and error formatting.
5. Preserve all Gutenberg APIs: `registerPlugin`, `PluginSidebarMoreMenuItem`, `PluginSidebar`, `Panel`, `TextareaControl`, `TextControl`, `CheckboxControl`, `Button`, `RawHTML`, `dispatch`, `select`, and `wp.data.withSelect`.
6. Preserve all DOM tags, component tags, `className` values, dynamic class branches, `role`, `aria-*`, `title`, `placeholder`, `style`, `dangerouslySetInnerHTML`, button labels, visible text, icons, SVG markup, and conditional render gates.
7. Preserve editor block attribute class mutations, especially `wpaw-diff-added` and `wpaw-diff-removed`; these are migration-critical even though they are not sidebar wrapper classes.
8. Do not rename functions, split files, normalize copy, deduplicate logic, replace SVGs, replace `RawHTML`, or change UX flow during the first migration pass.
9. Extraction is allowed only after a monolithic JSX port can be checked against this document line range by line range.
## File-Level Boundaries
| Lines | Required migration unit |
| --- | --- |
| 1-6 | File header comment. Preserve package context if the migrated file keeps source banner comments. |
| 7-15 | IIFE argument and WordPress dependency destructuring. New module imports may replace destructuring only if every dependency maps 1:1. |
| 16-30 | Debug logger and `isDebug` behavior. |
| 31-35 | `pluginIcon` image element with `wpAgenticWriter.pluginUrl + "/assets/img/icon.svg"`, alt text, and 20px style. |
| 37-12347 | `AgenticWriterSidebar` component. |
| 12349-12352 | `mapSelectToProps`, selecting `core/editor`. |
| 12355-12356 | `ConnectedSidebar = wp.data.withSelect(mapSelectToProps)(AgenticWriterSidebar)`. |
| 12358-12362 | `registerPlugin("wp-agentic-writer", { icon: pluginIcon, render: ConnectedSidebar })`. |
| 12363 | IIFE close with `window.wp`. |
## Migration Method
1. Create the new JSX target as a monolith first.
2. Copy ranges in the same order as the coverage table below.
3. Convert element calls mechanically:
- tag/component name stays the same
- prop names stay the same
- children order stays the same
- conditional gates stay in the same location
- spread/rest behavior is not introduced unless already present in the source range
4. After the monolith is complete, compare it against this document:
- every range is present
- every function exists
- every state/ref/effect exists in the same hook order
- every class in the class inventory exists
- every endpoint in the endpoint inventory exists
- every render surface exists with the same branches
5. Only after the monolith is proven equivalent, optional extraction may start. Extraction must move code, not rewrite behavior.
## Component-Level Coverage
### State and Refs, Lines 40-366
| Lines | Kind | Name |
| --- | --- | --- |
| 40-41 | const | `settings` |
| 42-138 | const | `formatAiErrorMessage` |
| 139-141 | state | `[activeTab, setActiveTab]` |
| 142-142 | state | `[messages, setMessages]` |
| 143-143 | state | `[input, setInput]` |
| 144-144 | state | `[isLoading, setIsLoading]` |
| 145-145 | state | `[currentSessionId, setCurrentSessionId]` |
| 146-146 | state | `[availableSessions, setAvailableSessions]` |
| 147-148 | state | `[isSessionActionLoading, setIsSessionActionLoading]` |
| 149-151 | state | `[agentMode, setAgentMode]` |
| 152-154 | const/ref | `tabIdRef` |
| 155-159 | state | `[sessionLock, setSessionLock]` |
| 160-162 | const/ref | `lockHeartbeatRef` |
| 163-181 | const/memo | `defaultPostConfig` |
| 182-182 | state | `[postConfig, setPostConfig]` |
| 183-183 | state | `[isConfigLoading, setIsConfigLoading]` |
| 184-184 | state | `[isConfigSaving, setIsConfigSaving]` |
| 185-185 | state | `[configError, setConfigError]` |
| 186-186 | const/ref | `configHydratedRef` |
| 187-187 | const/ref | `lastSavedConfigRef` |
| 188-190 | const/ref | `configSaveTimeoutRef` |
| 191-195 | state | `[cost, setCost]` |
| 196-200 | state | `[monthlyBudget, setMonthlyBudget]` |
| 201-203 | state | `[providerInfo, setProviderInfo]` |
| 204-223 | const | `applyProviderMetadata` |
| 224-224 | state | `[isEditorLocked, setIsEditorLocked]` |
| 225-225 | state | `[isRefinementLocked, setIsRefinementLocked]` |
| 226-226 | state | `[refiningBlockIds, setRefiningBlockIds]` |
| 227-227 | const/ref | `refinementDecoratedIdsRef` |
| 228-228 | const/ref | `lockedEditableNodesRef` |
| 229-229 | const/ref | `lockedBlockIdsRef` |
| 230-230 | const | `REFINEMENT_ALL_CONFIRM_THRESHOLD` |
| 231-235 | state | `[refineAllConfirm, setRefineAllConfirm]` |
| 236-236 | const/ref | `refineAllConfirmResolverRef` |
| 237-239 | const/ref | `skipRefineAllConfirmRef` |
| 240-240 | state | `[seoAudit, setSeoAudit]` |
| 241-241 | state | `[isSeoAuditing, setIsSeoAuditing]` |
| 242-242 | state | `[isGeneratingMeta, setIsGeneratingMeta]` |
| 243-245 | state | `[activeSeoFixKey, setActiveSeoFixKey]` |
| 246-246 | state | `[inClarification, setInClarification]` |
| 247-247 | state | `[questions, setQuestions]` |
| 248-248 | state | `[currentQuestionIndex, setCurrentQuestionIndex]` |
| 249-249 | state | `[answers, setAnswers]` |
| 250-250 | state | `[detectedLanguage, setDetectedLanguage]` |
| 251-252 | state | `[clarificationMode, setClarificationMode]` |
| 253-253 | state | `[pendingRefinement, setPendingRefinement]` |
| 254-254 | state | `[pendingEditPlan, setPendingEditPlan]` |
| 255-255 | state | `[pendingDiffBlockIds, setPendingDiffBlockIds]` |
| 256-256 | const/ref | `lastGenerationRequestRef` |
| 257-257 | const/ref | `currentPlanRef` |
| 258-258 | const/ref | `lastExecuteRequestRef` |
| 259-259 | const/ref | `sectionInsertIndexRef` |
| 260-260 | const/ref | `activeSectionIdRef` |
| 261-261 | const/ref | `sectionBlocksRef` |
| 262-262 | const/ref | `blockSectionRef` |
| 263-263 | const/ref | `markdownRendererRef` |
| 264-264 | const/ref | `lastRefineRequestRef` |
| 265-265 | const/ref | `lastChatRequestRef` |
| 266-266 | const/ref | `stopExecutionRef` |
| 267-267 | const/ref | `activeAbortControllerRef` |
| 268-268 | const/ref | `activeReaderRef` |
| 269-273 | const/ref | `activeOperationRef` |
| 274-274 | state | `[executionStopped, setExecutionStopped]` |
| 275-279 | state | `[activeOperation, setActiveOperation]` |
| 280-287 | state | `[writingState, setWritingState]` |
| 288-289 | state | `[isWritingStateLoading, setIsWritingStateLoading]` |
| 290-295 | state | `[workspaceSnapshot, setWorkspaceSnapshot]` |
| 296-307 | state | `[isWorkspaceCollapsed, setIsWorkspaceCollapsed]` |
| 308-323 | const | `toggleAgentWorkspace` |
| 324-325 | state | `[showMentionAutocomplete, setShowMentionAutocomplete]` |
| 326-326 | state | `[mentionQuery, setMentionQuery]` |
| 327-327 | state | `[mentionOptions, setMentionOptions]` |
| 328-328 | state | `[mentionCursorIndex, setMentionCursorIndex]` |
| 329-330 | state | `[showSlashAutocomplete, setShowSlashAutocomplete]` |
| 331-331 | state | `[slashQuery, setSlashQuery]` |
| 332-332 | state | `[slashOptions, setSlashOptions]` |
| 333-333 | state | `[slashCursorIndex, setSlashCursorIndex]` |
| 334-334 | state | `[isTextareaExpanded, setIsTextareaExpanded]` |
| 335-335 | const/ref | `inputRef` |
| 336-338 | const/ref | `streamTargetRef` |
| 339-340 | state | `[focusKeywordSuggestions, setFocusKeywordSuggestions]` |
| 341-341 | state | `[selectedFocusKeyword, setSelectedFocusKeyword]` |
| 342-343 | state | `[showCustomKeywordInput, setShowCustomKeywordInput]` |
| 344-344 | state | `[customKeywordInput, setCustomKeywordInput]` |
| 345-345 | const/ref | `messagesSaveTimeoutRef` |
| 346-346 | const/ref | `lastPersistedMessagesRef` |
| 347-349 | const/ref | `isHydratingSessionRef` |
| 350-350 | state | `[showWelcome, setShowWelcome]` |
| 351-351 | state | `[welcomeKeywordInput, setWelcomeKeywordInput]` |
| 352-354 | state | `[welcomeStartMode, setWelcomeStartMode]` |
| 355-355 | state | `[aiUndoStack, setAiUndoStack]` |
| 356-358 | const | `MAX_UNDO_STACK` |
| 359-365 | state | `[memantoRestore, setMemantoRestore]` |
| 366-366 | const/ref | `memantoRestoreFetchedRef` |
### Effects, Timeline, Editor Locking, Config Helpers, Lines 367-1194
| Lines | Kind | Name |
| --- | --- | --- |
| 367-372 | effect | `useEffect@367` |
| 373-401 | effect | `useEffect@373` |
| 402-448 | const | `savePostConfig` |
| 449-473 | effect | `useEffect@449` |
| 474-499 | effect | `useEffect@474` |
| 500-510 | const | `normalizeWritingState` |
| 511-539 | const | `saveWritingState` |
| 540-550 | const | `persistWritingStatePatch` |
| 551-585 | effect | `useEffect@551` |
| 586-586 | const/ref | `messagesEndRef` |
| 587-589 | const/ref | `messagesContainerRef` |
| 590-595 | effect | `useEffect@590` |
| 596-597 | const | `progressRegex` |
| 598-608 | const | `activeTimelineStatuses` |
| 609-609 | const | `writingTimelineStatuses` |
| 610-621 | const | `findLastActiveTimelineIndex` |
| 622-636 | const | `deactivateActiveTimelineEntries` |
| 637-659 | const | `updateOrCreateTimelineEntry` |
| 660-673 | const | `addActivityTimeline` |
| 674-682 | const | `setActiveOperationState` |
| 683-691 | const | `beginAgentOperation` |
| 692-700 | const | `finishAgentOperation` |
| 701-721 | const | `markActiveOperationStopping` |
| 722-724 | const | `isAbortError` |
| 725-728 | const | `registerActiveReader` |
| 729-742 | callback | `requestRefineAllConfirmation` |
| 743-752 | callback | `resolveRefineAllConfirmation` |
| 753-764 | const | `captureEditorSnapshot` |
| 765-775 | const | `pushUndoSnapshot` |
| 776-812 | const | `undoLastAiOperation` |
| 813-832 | effect | `useEffect@813` |
| 833-841 | effect | `useEffect@833` |
| 842-889 | effect | `useEffect@842` |
| 890-921 | effect | `useEffect@890` |
| 922-981 | effect | `useEffect@922` |
| 982-1019 | effect | `useEffect@982` |
| 1020-1028 | const | `toTextValue` |
| 1029-1031 | const | `updatePostConfig` |
| 1032-1071 | const | `buildPostConfigFromAnswers` |
| 1072-1079 | const | `handleFocusKeywordChange` |
| 1080-1089 | const | `handleKeywordSelect` |
| 1090-1137 | const | `extractFocusKeywordSuggestions` |
| 1138-1142 | const | `extractFocusKeywordSuggestion` |
| 1143-1153 | const | `addFocusKeywordSuggestion` |
| 1154-1159 | const | `addFocusKeywordSuggestions` |
| 1160-1168 | effect | `useEffect@1160` |
| 1169-1175 | effect | `useEffect@1169` |
| 1176-1194 | const | `handleWelcomeStart` |
### SEO, Workspace, Session, Locking, Lines 1195-2515
| Lines | Kind | Name |
| --- | --- | --- |
| 1195-1269 | const | `runSeoAudit` |
| 1270-1308 | const | `buildSeoAuditFixInstruction` |
| 1309-1310 | const | `getSeoFixKey` |
| 1311-1318 | const | `getSeoAuditPatternCount` |
| 1319-1322 | const | `formatCountLabel` |
| 1323-1328 | const | `formatAuditPatternLabel` |
| 1329-1348 | const | `buildAuditRefinementContext` |
| 1349-1459 | const | `handleSeoAuditFix` |
| 1460-1563 | const | `generateMetaDescription` |
| 1564-1587 | const | `extractBlockPreview` |
| 1588-1599 | const | `getBlockPreviewById` |
| 1600-1638 | callback | `buildWorkspaceSnapshot` |
| 1639-1661 | effect | `useEffect@1639` |
| 1662-1668 | effect | `useEffect@1662` |
| 1669-1682 | effect | `useEffect@1669` |
| 1683-1686 | effect | `useEffect@1683` |
| 1687-1702 | effect | `useEffect@1687` |
| 1703-1716 | effect | `useEffect@1703` |
| 1717-1754 | callback | `sanitizeMessagesForStorage` |
| 1755-1781 | callback | `hydrateSessionStateFromMessages` |
| 1782-1839 | callback | `persistSessionMessages` |
| 1840-1840 | const/ref | `messagesRef` |
| 1841-1845 | effect | `useEffect@1841` |
| 1846-1889 | effect | `useEffect@1846` |
| 1890-1926 | callback | `acquireSessionLock` |
| 1927-1947 | callback | `releaseSessionLock` |
| 1948-1961 | callback | `startLockHeartbeat` |
| 1962-1969 | callback | `stopLockHeartbeat` |
| 1970-1994 | effect | `useEffect@1970` |
| 1995-2026 | callback | `takeOverSession` |
| 2027-2072 | effect | `useEffect@2027` |
| 2073-2213 | effect | `useEffect@2073` |
| 2214-2241 | effect | `useEffect@2214` |
| 2242-2300 | effect | `useEffect@2242` |
| 2301-2397 | const | `loadPostSessions` |
| 2398-2515 | const | `openSessionById` |
### Mentions, Commands, Planning, Agent Decisions, Lines 2516-4320
| Lines | Kind | Name |
| --- | --- | --- |
| 2516-2526 | const | `resolveStreamTarget` |
| 2527-2536 | const | `normalizeMentionToken` |
| 2537-2550 | const | `extractMentionsFromText` |
| 2551-2560 | const | `stripMentionsFromText` |
| 2561-2569 | const | `hasTitleMention` |
| 2570-2689 | const | `handleTitleRefinement` |
| 2690-2712 | const | `parseInsertCommand` |
| 2713-2747 | const | `getSlashOptions` |
| 2748-2758 | const | `getBlockIndex` |
| 2759-2777 | const | `resolveTargetBlockId` |
| 2778-2870 | const | `insertRefinementBlock` |
| 2871-3188 | const | `streamGeneratePlan` |
| 3189-3213 | const | `retryLastGeneration` |
| 3214-3228 | const | `retryLastExecute` |
| 3229-3257 | const | `retryLastRefinement` |
| 3258-3473 | const | `retryLastChat` |
| 3474-3534 | const | `createBlockFromPlan` |
| 3535-3543 | const | `normalizePlanActions` |
| 3544-3604 | const | `buildPlanPreviewItem` |
| 3605-3611 | const | `normalizePlanSectionTitle` |
| 3612-3622 | const | `upsertSectionBlock` |
| 3623-3632 | const | `removeSectionBlock` |
| 3633-3673 | const | `loadSectionBlocks` |
| 3674-3696 | const | `saveSectionBlocks` |
| 3697-3709 | const | `ensurePlanTasks` |
| 3710-3743 | const | `getTargetedRefinementBlocks` |
| 3744-3828 | const | `findBestPlanSectionMatch` |
| 3829-3851 | const | `updatePlanSectionStatus` |
| 3852-3893 | const | `findSectionInsertIndex` |
| 3894-3906 | const | `shouldShowWritingEmptyState` |
| 3907-3956 | const | `summarizeChatHistory` |
| 3957-4007 | const | `detectUserIntent` |
| 4008-4027 | const | `buildOptimizedContext` |
| 4028-4067 | const | `handleResetCommand` |
| 4068-4096 | const | `updateOrCreatePlanMessage` |
| 4097-4156 | const | `suggestKeywordsFromPlan` |
| 4157-4172 | callback | `buildChatHistoryPayload` |
| 4173-4186 | callback | `getLastUserMessageText` |
| 4187-4198 | const | `shouldSkipPlanningCompletion` |
| 4199-4218 | const | `getPlanRuntimeSummary` |
| 4219-4221 | const | `getPlanId` |
| 4222-4260 | const | `classifyAgentIntent` |
| 4261-4320 | const | `decideAgentAction` |
### Execution, Refinement, Block Context, Lines 4321-6388
| Lines | Kind | Name |
| --- | --- | --- |
| 4321-4768 | const | `executePlanFromCard` |
| 4769-4794 | const | `handleStopExecution` |
| 4795-4870 | const | `clearChatContext` |
| 4871-4953 | const | `createBlocksFromSerialized` |
| 4954-5056 | const | `reformatBlocks` |
| 5057-5142 | const | `revisePlanFromPrompt` |
| 5143-5240 | const | `applyEditPlan` |
| 5241-5280 | const | `cancelEditPlan` |
| 5281-5304 | const | `formatClarificationContext` |
| 5305-5327 | effect | `useEffect@5305` |
| 5328-5362 | const | `removeDuplicateHeadings` |
| 5363-5390 | const | `getRefineableBlocks` |
| 5391-5420 | const | `getListItemBlocks` |
| 5421-5426 | const | `resolveExplicitListItem` |
| 5427-5442 | const | `getParentListId` |
| 5443-5453 | const | `getBlockContentForContext` |
| 5454-5470 | const | `getHeadingContextForBlock` |
| 5471-5494 | const | `getNearbyParagraphContext` |
| 5495-5503 | const | `getContextFromMentions` |
| 5504-5518 | const | `extractQuotedTermsFromMessage` |
| 5519-5520 | const | `getAllTextRefineableBlocks` |
| 5521-5533 | const | `selectLikelySlangBlocks` |
| 5534-5537 | const | `isAiSlopRequest` |
| 5538-5589 | const | `getAiSlopFindingsForBlock` |
| 5590-5606 | const | `selectLikelyAiSlopBlocks` |
| 5607-5622 | const | `buildContextBlocksForRefinement` |
| 5623-5704 | const | `buildRefinementDiagnosis` |
| 5705-5799 | const | `resolveBlockMentions` |
| 5800-6388 | const | `handleChatRefinement` |
### Chat Input, Send, Clarification, Welcome, Workspace, Lines 6389-9996
| Lines | Kind | Name |
| --- | --- | --- |
| 6389-6455 | render | `renderRefineAllConfirmModal` |
| 6456-6579 | const | `getMentionOptions` |
| 6580-6612 | effect | `useEffect@6580` |
| 6613-6652 | const | `handleInputChange` |
| 6653-6701 | const | `handleKeyDown` |
| 6702-6726 | const | `insertMention` |
| 6727-6758 | const | `insertSlashCommand` |
| 6759-8056 | const | `sendMessage` |
| 8057-8476 | const | `submitAnswers` |
| 8477-8829 | render | `renderClarification` |
| 8830-8916 | const | `startNewConversation` |
| 8917-8959 | const | `deleteConversationSession` |
| 8960-8983 | const | `getSessionDisplayTitle` |
| 8984-8993 | const | `getSessionContinuityLabel` |
| 8994-9004 | const | `getSessionDebugMeta` |
| 9005-9184 | render | `renderWelcomeScreen` |
| 9185-9253 | render | `renderWritingEmptyState` |
| 9254-9459 | render | `renderFocusKeywordBar` |
| 9460-9629 | render | `renderAgentWorkspaceCard` |
| 9630-9632 | alias | `renderContextIndicator = renderAgentWorkspaceCard` |
| 9633-9996 | render/action | `renderContextualAction` |
### Messages, Tabs, Cost, Final Shell, Lines 9997-12363
| Lines | Kind | Name |
| --- | --- | --- |
| 9997-10917 | render | `renderMessages` |
| 10918-11434 | render | `renderConfigTab` |
| 11435-11448 | const | `getAgentStatus` |
| 11449-11585 | render | `renderGlobalStatusBar` |
| 11586-12020 | render | `renderChatTab` |
| 12021-12022 | state | `[costHistory, setCostHistory]` |
| 12023-12050 | const | `refreshCostData` |
| 12051-12057 | effect | `useEffect@12051` |
| 12058-12306 | render | `renderCostTab` |
| 12307-12346 | return | component `Fragment` with menu item, sidebar, panel, and active tab renderer |
| 12347 | close | `AgenticWriterSidebar` close |
| 12349-12363 | boot | `mapSelectToProps`, `ConnectedSidebar`, `registerPlugin`, IIFE close |
## Render Surface Checklist
Each render surface must be migrated as an element tree, not summarized.
| Render surface | Lines | Required branches/elements |
| --- | --- | --- |
| `renderRefineAllConfirmModal` | 6389-6455 | Null when closed; dialog overlay; modal; title; body; `CheckboxControl`; cancel and continue `Button`s; session skip flag behavior. |
| `renderClarification` | 8477-8829 | Null guard; `renderSingleChoice`; custom option textarea; `renderMultipleChoice`; `renderOpenText`; `renderConfigForm`; answer switch; quiz wrapper; progress bar; previous/skip/next-finish buttons. |
| `renderWelcomeScreen` | 9005-9184 | Recent session button; older sessions details list; session open/delete buttons; focus keyword input; chat/planning mode pills; start button. |
| `renderWritingEmptyState` | 9185-9253 | Empty state wrapper; SVG icon; title; paragraph; create outline button with inline SVG; hint paragraph. |
| `renderFocusKeywordBar` | 9254-9459 | Expanded mode branch; compact mode branch; keyword input behavior; suggestions; selected state; cost/provider indicators; expand/collapse controls. |
| `renderAgentWorkspaceCard` | 9460-9629 | Workspace status; collapsed state; context grid; keyword field; conversation/provider summaries; resume card. |
| `renderContextIndicator` | 9630-9632 | Alias to `renderAgentWorkspaceCard`, not a new implementation. |
| `renderContextualAction` | 9633-9996 | Null guard; `create_outline` action object; clarity check; plan generation stream; contextual action card. |
| `renderMessages` | 9997-10917 | Markdown helpers; grouping logic; user messages; AI group; timeline entries; plan cards; edit-plan cards; structured errors; normal response; resume actions. |
| `renderConfigTab` | 10918-11434 | Configuration wrapper; article length; language; tone; experience; image/search toggles; SEO section; meta generation; SEO audit result/fix UI; status descriptions. |
| `renderGlobalStatusBar` | 11449-11585 | Status dot/label; memory badge; undo; sessions; chat; workspace toggle; config; cost icon buttons. |
| `renderChatTab` | 11586-12020 | Lock banners; health notices; welcome/empty/workspace/activity-log gates; command area; hint; textarea; mention/slash autocomplete; search toggle; stop/send buttons; keyboard hints; refine modal. |
| `renderCostTab` | 12058-12306 | Cost header; refresh; cost cards; budget section; warning; action summary table; history table; settings footer link. |
| Main return | 12307-12346 | `Fragment`; `PluginSidebarMoreMenuItem`; `PluginSidebar`; icon title; `Panel`; `wpaw-tab-content-wrapper`; active tab switch. |
## Nested Function Checklist
These nested helpers are easy to lose during component extraction.
| Parent | Lines | Nested item |
| --- | --- | --- |
| `renderClarification` | 8486-8547 | `renderSingleChoice` |
| `renderClarification` | 8550-8577 | `renderMultipleChoice` |
| `renderClarification` | 8580-8597 | `renderOpenText` |
| `renderClarification` | 8600-8723 | `renderConfigForm` |
| `renderClarification` | 8725-8742 | `answerInput` switch |
| `renderContextualAction` | 9636-9966 | `actions.create_outline` object and async `onClick` |
| `renderMessages` | 9998-10006 | `normalizeMessageContent` |
| `renderMessages` | 10007-10014 | `escapeHtml` |
| `renderMessages` | 10015-10031 | `inlineMarkdownToHtml` |
| `renderMessages` | 10032-10270 | `markdownToHtml` |
| `markdownToHtml` | 10112-10117 | `flushParagraph` |
| `markdownToHtml` | 10118-10140 | `flushList` |
| `markdownToHtml` | 10141-10144 | `addListItem` |
| `markdownToHtml` | 10145-10156 | `addDetailToLastItem` |
| `markdownToHtml` | 10158-10166 | `getListType` |
| `renderMessages` | 10271-10276 | `renderMessageContent` |
| `renderMessages` | 10278-10312 | group building and user message branch |
| `renderMessages` | 10313-10482 | AI group and timeline branch |
| `renderMessages` | 10485-10645 | `message.type === "plan"` branch |
| `renderMessages` | 10647-10767 | `message.type === "edit_plan"` branch |
| `renderMessages` | 10769-10848 | `message.type === "error"` branch |
| `renderMessages` | 10850-10909 | default AI response branch |
| `renderConfigTab` | 10989-10990 | language list merging |
| `renderCostTab` | 12059-12079 | budget percent/status and action summary locals |
## Endpoint Inventory
Every endpoint path below is used by `sidebar.js` and must remain present with its existing method, headers, body shape, and response handling.
| Lines | Endpoint |
| --- | --- |
| 379, 411-412 | `/post-config/${postId}` |
| 479, 12026-12027 | `/cost-tracking/${postId}` |
| 519-520, 558 | `/writing-state/${postId}` |
| 1200-1201 | `/seo-audit/${postId}` |
| 1468-1469 | `/generate-meta` |
| 1813-1814, 1874, 1893-1894, 1931-1932, 1979, 1999-2000, 2122-2123, 2308-2309, 2335-2336, 2413-2414, 2479-2480, 4808-4809, 8836-8837, 8926-8927 | conversation endpoints |
| 2143-2144, 2448-2449 | `/conversation/${postId}` and `/conversation/${data.post_id}` |
| 2167-2168 | `/chat-history/${postId}` |
| 2220 | `/memanto/restore?post_id=${postId}` |
| 2612 | `/refine-title` |
| 2897-2898, 7354-7355, 7762-7763, 8158-8159, 9809-9810 | `/generate-plan` |
| 3288, 6922 | `/chat` |
| 3638-3639, 3680 | `/section-blocks/${postId}` and `/section-blocks` |
| 3915-3916 | `/summarize-context` |
| 3963-3964 | `/detect-intent` |
| 4039 | `/clear-context` |
| 4103-4104 | `/suggest-keywords` |
| 4431-4432 | `/execute-article` |
| 4984-4985 | `/reformat-blocks` |
| 5087 | `/revise-plan` |
| 5969-5970 | `/refine-from-chat` |
| 7251-7252, 9681-9682 | `/check-clarity` |
## Editor and Browser Side Effects
Preserve these integrations exactly.
| Lines | Side effect |
| --- | --- |
| 300-314 | `localStorage` key `wpaw_agent_workspace_collapsed`. |
| 449-473 | Debounced config save via `configSaveTimeoutRef`. |
| 683-700, 4769-4794 | `AbortController` lifecycle and active operation state. |
| 813-841 | Post saving lock/unlock for writing and refining. |
| 842-981 | Editor input lock, block decoration, and global key/paste/drop/cut blockers. |
| 1651-1653 | `wp.data.subscribe` workspace snapshot update. |
| 1678-1680, 1846-1889, 1970-1994 | `beforeunload` handlers. |
| 1692, 1708, 2095 | `localStorage` session keys. |
| 1948-1969 | Session lock heartbeat interval. |
| 2871-3188, 3258-3473, 4321-4768, 5800-6388, 6759-8056, 8057-8476, 9633-9996 | Stream readers, `TextDecoder`, timeout cleanup, and abort checks. |
| 6580-6612 | Window event `wpaw:insert-mention`. |
| 9400-9404, 9563-9567 | Focus keyword debounce saves. |
| 12349-12356 | `wp.data.withSelect` connection to current post ID. |
## Class Inventory
All classes below are present in `sidebar.js`; preserve spelling and combinations. First column is the first line where the class appears.
```text
6075 wpaw-diff-removed
6088 wpaw-diff-added
6129 wpaw-diff-added
6397 wpaw-refine-confirm-overlay
6404 wpaw-refine-confirm-modal
6407 wpaw-refine-confirm-title
6412 wpaw-refine-confirm-body
6428 wpaw-refine-confirm-actions
8493 wpaw-answer-options
8515 wpaw-custom-answer-wrapper
8533 wpaw-custom-text-input
8631 wpaw-config-form
8646 wpaw-config-field
8653 wpaw-config-label
8656 wpaw-config-label-text
8662 wpaw-config-description
8668 wpaw-config-toggle
8682 wpaw-toggle-slider
8706 wpaw-config-text-input
8746 wpaw-clarification-quiz
8746 dark-theme
8749 wpaw-quiz-header
8753 wpaw-progress-bar
8755 wpaw-progress-fill
8770 wpaw-question-card
8775 wpaw-quiz-actions
9011 wpaw-welcome-screen
9014 wpaw-welcome-content
9016 wpaw-welcome-icon
9024 wpaw-welcome-title
9029 wpaw-welcome-subtitle
9037 wpaw-welcome-pill
9065 wpaw-session-list
9086 wpaw-session-open-btn
9135 wpaw-welcome-input
9148 wpaw-welcome-pills
9176 wpaw-welcome-start-btn
9188 wpaw-writing-empty-state
9191 wpaw-empty-state-content
9193 wpaw-empty-state-icon
9210 wpaw-empty-state-button
9244 wpaw-empty-state-hint
9262 wpaw-focus-keyword-bar
9262 wpaw-expanded
9266 wpaw-fk-header
9271 wpaw-fk-collapse
9281 wpaw-fk-main-input
9284 wpaw-fk-custom-input
9311 wpaw-fk-suggestions
9314 wpaw-fk-suggestions-label
9322 wpaw-fk-suggestion-item
9324 selected
9329 wpaw-fk-radio
9334 wpaw-fk-suggestion-text
9339 wpaw-fk-suggestion-source
9348 wpaw-fk-stats
9358 wpaw-provider-info
9370 wpaw-fk-divider
9385 wpaw-compact
9388 wpaw-fk-left
9389 wpaw-fk-icon
9392 wpaw-fk-input
9417 wpaw-fk-cost
9423 wpaw-provider-badge
9435 wpaw-fk-expand
9489 wpaw-agent-workspace-card
9489 is-collapsed
9493 wpaw-agent-workspace-header
9496 wpaw-agent-workspace-heading
9499 wpaw-agent-workspace-kicker
9504 wpaw-agent-workspace-title
9510 wpaw-agent-workspace-actions
9514 wpaw-agent-workspace-status
9514 status-${activeWorkspaceStatus}
9523 wpaw-agent-context-grid
9526 wpaw-agent-context-item
9556 wpaw-agent-keyword-input
9598 wpaw-agent-resume-card
9973 wpaw-contextual-action
9976 wpaw-action-icon
9981 wpaw-action-content
10303 wpaw-message
10303 wpaw-message-user
10307 wpaw-message-content
10343 wpaw-ai-response
10352 complete
10355 inactive
10356 active
10372 wpaw-ai-item
10372 wpaw-timeline-entry
10375 is-current
10378 wpaw-timeline-dot
10383 wpaw-timeline-content
10386 wpaw-timeline-message
10393 wpaw-timeline-complete-row
10402 wpaw-timeline-complete
10407 wpaw-timeline-elapsed
10421 wpaw-inline-undo-btn
10452 wpaw-processing-indicator
10454 wpaw-dots-loader
10469 wpaw-typing-indicator
10475 wpaw-typing-dots
10555 wpaw-plan-card
10559 wpaw-plan-title
10564 wpaw-plan-config-summary
10568 wpaw-config-summary-item
10576 wpaw-plan-sections
10582 wpaw-plan-section
10582 pending
10582 done
10582 in_progress
10586 wpaw-plan-section-row
10588 wpaw-plan-section-check
10596 wpaw-plan-section-body
10599 wpaw-plan-section-title
10607 wpaw-plan-section-desc
10613 wpaw-plan-section-status
10633 wpaw-plan-actions
10676 wpaw-edit-plan
10680 wpaw-edit-plan-title
10685 wpaw-edit-plan-summary
10691 wpaw-edit-plan-preview-label
10697 wpaw-edit-plan-list
10703 wpaw-edit-plan-item
10708 wpaw-edit-plan-item-title
10718 wpaw-edit-plan-item-target
10746 wpaw-edit-plan-actions
10797 wpaw-message-error
10805 wpaw-error-title
10812 wpaw-error-detail
10854 wpaw-response
10858 wpaw-response-content
10884 wpaw-resume-actions
10923 wpaw-tab-content
10923 wpaw-config-tab
10927 wpaw-tab-header
10933 wpaw-config-section
10937 description
10952 wpaw-select
11093 wpaw-config-divider
11161 wpaw-meta-info
11168 good
11169 warning
11192 wpaw-spinning-icon
11210 wpaw-svg-wrapper
11226 wpaw-seo-audit
11229 wpaw-seo-audit-header
11285 wpaw-seo-audit-results
11289 wpaw-seo-score
11295 poor
11299 score-value
11304 score-label
11310 wpaw-seo-stats
11313 wpaw-seo-stat
11316 stat-label
11321 stat-value
11343 wpaw-seo-checks
11352 wpaw-seo-check
11354 passed
11354 failed
11358 check-icon
11363 check-label
11372 wpaw-seo-fix-button
11374 is-fixing
11465 wpaw-status-bar
11471 wpaw-status-indicator
11473 wpaw-status-dot
11477 wpaw-status-label
11486 wpaw-memanto-badge
11495 wpaw-status-actions
11499 wpaw-status-icon-btn
11499 wpaw-undo-btn
11499 has-undo
11512 is-active
11544 wpaw-workspace-toggle-btn
11592 wpaw-chat-tab
11596 wpaw-chat-container
11596 is-dimmed
11603 wpaw-editor-lock-banner
11609 wpaw-refinement-lock-banner
11616 wpaw-session-lock-banner
11627 wpaw-session-lock-takeover
11639 wpaw-health-notice
11670 wpaw-messages
11670 wpaw-activity-log
11674 wpaw-messages-inner
11687 wpaw-command-area
11695 wpaw-input-hint
11695 is-hidden
11707 wpaw-command-input-wrapper
11709 expanded
11713 wpaw-command-prefix
11718 wpaw-input
11758 wpaw-mention-autocomplete
11777 wpaw-mention-option
11877 wpaw-command-actions
11881 wpaw-command-actions-group
11903 wpaw-web-search-toggle
11905 wpaw-search-blocked
11931 wpaw-web-search-icon
11939 wpaw-web-search-label
11953 wpaw-command-circle-btn
11954 wpaw-stop-circle-btn
11955 is-stopping
11964 wpaw-stop-spinner
11972 wpaw-send-circle-btn
11986 wpaw-keyboard-hints
11989 wpaw-kbd
12062 ok
12062 danger
12083 wpaw-cost-tab
12089 wpaw-refresh-btn
12100 wpaw-cost-card
12103 wpaw-cost-stat
12107 wpaw-cost-value
12125 wpaw-cost-remaining
12137 wpaw-budget-section
12140 wpaw-budget-label
12156 wpaw-budget-bar
12158 wpaw-budget-fill
12167 wpaw-budget-warning
12176 wpaw-cost-history
12181 wpaw-cost-table-wrapper
12186 wpaw-cost-table
12285 wpaw-cost-footer
12293 wpaw-cost-settings-link
12338 wpaw-tab-content-wrapper
```
## Non-Render Class and Selector Inventory
These names are not all sidebar `className` props, but they are still source-of-truth migration items.
| Lines | Name | Required preservation |
| --- | --- | --- |
| 824, 828 | `wpaw-writing` | WordPress post-saving lock key. |
| 825, 829 | `wpaw-editor-locked` | Body class added/removed while writing lock is active. |
| 835, 838 | `wpaw-refining` | WordPress post-saving lock key. |
| 836, 839 | `wpaw-refining-locked` | Body class added/removed while refinement lock is active. |
| 895, 903, 916 | `wpaw-block-refining` | Editor block DOM class added/removed for refining blocks. |
| 933 | `.wpaw-sidebar` | Selector allowlist for editor input blocking. Preserve alongside `.wpaw-command-area` and `.wpaw-messages`. |
| 5262 | `wpaw-diff-removed` | Removed from block `className` during diff cleanup. |
## Dynamic Class Expressions
These expressions must be migrated as expressions, not flattened.
| Lines | Expression behavior |
| --- | --- |
| 6075-6078, 6111-6114 | Append `wpaw-diff-removed` to editor block `className`. |
| 6085-6089, 6128-6129 | Append `wpaw-diff-added` to generated diff blocks. |
| 9152-9154 | Welcome chat pill adds `active`. |
| 9162-9164 | Welcome planning pill adds `active`. |
| 9322-9324 | Focus keyword suggestion item adds `selected`. |
| 9489 | Workspace card adds `is-collapsed`. |
| 9514 | Workspace status uses `status-${activeWorkspaceStatus}`. |
| 10372-10375 | Timeline entry combines `wpaw-ai-item wpaw-timeline-entry`, `statusClass`, and `is-current`. |
| 10582 | Plan section uses `section.status || "pending"`. |
| 11165-11169 | Meta character count uses `good` or `warning`. |
| 11289-11295 | SEO score uses `good`, `warning`, or `poor`. |
| 11352-11354 | SEO check uses `passed` or `failed`. |
| 11372-11374 | SEO fix button adds `is-fixing`. |
| 11473 | Status dot appends `agentStatus`. |
| 11510-11512 | Sessions icon button adds `is-active`. |
| 11527-11529 | Chat icon button adds `is-active`. |
| 11556-11558 | Config icon button adds `is-active`. |
| 11570-11572 | Cost icon button adds `is-active`. |
| 11596 | Chat container adds `is-dimmed`. |
| 11695 | Input hint adds `is-hidden`. |
| 11707-11709 | Command input wrapper adds `expanded`. |
| 11777-11779 | Mention autocomplete option adds `selected`. |
| 11838-11840 | Slash autocomplete option adds `selected`. |
| 11903-11905 | Web search toggle adds `wpaw-search-blocked`. |
| 11953-11955 | Stop button adds `is-stopping`. |
| 12129, 12158, 12167 | Cost/budget UI appends `budgetStatus`. |
## Migration Acceptance Checklist
- [ ] `sidebar.js` line ranges 1-12363 are represented in the migrated plan or monolith.
- [ ] The component starts from `AgenticWriterSidebar` line 38 and closes at line 12347.
- [ ] Final HOC/plugin registration lines 12349-12363 are represented separately.
- [ ] Every state/ref/effect in the coverage table exists in the same order.
- [ ] Every function in the coverage table exists under the same name.
- [ ] Every nested helper in the nested checklist exists.
- [ ] Every render surface in the render checklist exists with the same branch gates.
- [ ] Every class in the class inventory exists, including dynamic and editor block classes.
- [ ] Every endpoint in the endpoint inventory exists with the same request semantics.
- [ ] Every editor/browser side effect in the side-effect inventory exists.
- [ ] No refactor-only cleanup has been mixed into the first migration pass.

296
SIDEBAR_1_TO_1_TASKLIST.md Normal file
View File

@@ -0,0 +1,296 @@
# WP Agentic Writer Sidebar 1:1 Migration Tasklist
**Source task plan:** `SIDEBAR_1_TO_1_MIGRATION.md`
**Source of truth:** `assets/js/sidebar.js`
**Mode:** planning/tasklist only. No implementation code, no build step, no inferred behavior.
Use this tasklist to execute the migration described in `SIDEBAR_1_TO_1_MIGRATION.md`. Every checkbox must be completed against `assets/js/sidebar.js`, not memory.
**Current status:** planning docs are in place and the abandoned React rebuild scaffold has been removed from `assets/js`. No build was run, no enqueue switch was made, and implementation now needs to restart from the migration plan rather than from a preexisting React target.
## Ground Rules
- [x] Confirm `assets/js/sidebar.js` is the current source of truth before starting.
- [x] Confirm `SIDEBAR_1_TO_1_MIGRATION.md` is open beside `assets/js/sidebar.js`.
- [x] Do not use `MIGRATION_GUIDE.md` as authority when it differs from `assets/js/sidebar.js`.
- [x] Do not rename functions during the first migration pass.
- [x] Do not extract helper files during the first migration pass.
- [x] Do not change visible text, labels, placeholders, icons, SVG markup, classes, inline styles, aria attributes, roles, or conditional render gates.
- [x] Do not replace `RawHTML`, markdown logic, stream handling, abort handling, localStorage keys, endpoint paths, request bodies, or Gutenberg APIs.
- [ ] Keep the first target as a monolithic JSX port.
## Definition of Done for Each Range
- [ ] Source range is copied or represented completely.
- [ ] `createElement` conversion keeps the same tag/component.
- [ ] Props are preserved 1:1.
- [ ] Children order is preserved 1:1.
- [ ] Conditional gates are preserved in the same branch position.
- [ ] State/ref/effect ordering is preserved.
- [ ] Callback boundaries and dependency arrays are preserved.
- [ ] Inline styles, string literals, titles, placeholders, labels, and SVG strings are preserved.
- [ ] Any migrated range is checked against the original source lines before moving on.
## Phase 0 - Source Lock and Boundaries
- [x] Verify `sidebar.js` has 12,363 lines.
- [x] Verify file header and IIFE boundary: lines 1-7 and 12363.
- [x] Verify dependency destructuring: lines 8-15.
- [x] Verify debug logger and `pluginIcon`: lines 16-35.
- [x] Verify `AgenticWriterSidebar` starts at line 38.
- [x] Verify `AgenticWriterSidebar` closes at line 12347.
- [x] Verify HOC/plugin boot remains separate: lines 12349-12363.
- [x] Verify main return starts at line 12307.
- [x] Record any source drift before implementation. If line numbers changed, regenerate this tasklist from the updated source.
## Phase 1 - Monolith Skeleton
- [ ] Create the migrated target as one monolithic sidebar component.
- [ ] Map every WordPress dependency from lines 8-15.
- [ ] Preserve `pluginIcon` behavior from lines 31-35.
- [ ] Preserve component prop shape: `AgenticWriterSidebar = ({ postId })`.
- [ ] Preserve `mapSelectToProps` behavior from lines 12349-12352.
- [ ] Preserve `ConnectedSidebar` behavior from lines 12355-12356.
- [ ] Preserve `registerPlugin("wp-agentic-writer")` behavior from lines 12358-12362.
- [ ] Do not split files yet.
## Phase 2 - State, Refs, and Constants
- [ ] Migrate settings and error formatting: lines 40-138.
- [ ] Migrate chat/top-level state: lines 139-149.
- [ ] Migrate session lock state and refs: lines 152-160.
- [ ] Migrate config defaults, config state, and config refs: lines 163-188.
- [ ] Migrate cost/provider state and provider metadata helper: lines 191-223.
- [ ] Migrate editor/refinement lock state and refs: lines 224-237.
- [ ] Migrate SEO audit state: lines 240-243.
- [ ] Migrate clarification, pending plan, and request refs: lines 246-268.
- [ ] Migrate active operation refs/state: lines 269-279.
- [ ] Migrate writing state: lines 280-289.
- [ ] Migrate workspace snapshot/collapse state and toggle: lines 290-321.
- [ ] Migrate mention/slash/input refs and state: lines 323-336.
- [ ] Migrate focus keyword state and persistence refs: lines 338-347.
- [ ] Migrate welcome state: lines 349-352.
- [ ] Migrate undo stack and max size: lines 354-356.
- [ ] Migrate Memanto restore state/ref: lines 358-366.
- [ ] Verify state/ref/hook order matches `sidebar.js` exactly.
## Phase 3 - Effects, Saving, Timeline, and Editor Locks
- [ ] Migrate agent mode reset effect: lines 367-371.
- [ ] Migrate post config load effect: lines 373-400.
- [ ] Migrate `savePostConfig`: lines 402-448.
- [ ] Migrate debounced post config effect: lines 449-473.
- [ ] Migrate cost tracking effect: lines 474-499.
- [ ] Migrate `normalizeWritingState`: lines 500-510.
- [ ] Migrate `saveWritingState`: lines 511-539.
- [ ] Migrate `persistWritingStatePatch`: lines 540-550.
- [ ] Migrate writing state load effect: lines 551-585.
- [ ] Migrate scroll refs/effect: lines 586-595.
- [ ] Migrate timeline regex/status constants and helpers: lines 596-673.
- [ ] Migrate active operation helpers: lines 674-728.
- [ ] Migrate refine-all confirmation callbacks: lines 729-752.
- [ ] Migrate undo snapshot helpers: lines 753-812.
- [ ] Migrate post saving lock effects: lines 813-841.
- [ ] Migrate editor input lock and DOM blocker effect: lines 842-981.
- [ ] Migrate text/config/keyword helpers and welcome start: lines 1020-1194.
## Phase 4 - SEO, Workspace, Sessions, and Locks
- [ ] Migrate `runSeoAudit`: lines 1195-1269.
- [ ] Migrate SEO fix instruction helpers: lines 1270-1348.
- [ ] Migrate `handleSeoAuditFix`: lines 1349-1459.
- [ ] Migrate `generateMetaDescription`: lines 1460-1563.
- [ ] Migrate block preview helpers: lines 1564-1599.
- [ ] Migrate workspace snapshot helper/effects: lines 1600-1668.
- [ ] Migrate beforeunload/session restore effects: lines 1669-1716.
- [ ] Migrate session storage sanitizing and hydration: lines 1717-1781.
- [ ] Migrate session message persistence: lines 1782-1889.
- [ ] Migrate session lock acquire/release/heartbeat/takeover: lines 1890-2026.
- [ ] Migrate lock/chat-history/post-session effects: lines 2027-2300.
- [ ] Migrate `loadPostSessions`: lines 2301-2397.
- [ ] Migrate `openSessionById`: lines 2398-2515.
## Phase 5 - Mentions, Commands, Plans, and Agent Decisions
- [ ] Migrate stream target and mention token helpers: lines 2516-2569.
- [ ] Migrate title refinement: lines 2570-2689.
- [ ] Migrate slash command parsing/options/block index: lines 2690-2758.
- [ ] Migrate target block resolution and insert refinement block: lines 2759-2870.
- [ ] Migrate plan streaming: lines 2871-3188.
- [ ] Migrate retry helpers: lines 3189-3473.
- [ ] Migrate plan block/preview helpers: lines 3474-3604.
- [ ] Migrate section block helpers and persistence: lines 3605-3696.
- [ ] Migrate plan task/target/matching/status/index helpers: lines 3697-3906.
- [ ] Migrate chat summary, intent detection, context, reset, plan message, keyword suggestion, and runtime helpers: lines 3907-4218.
- [ ] Migrate plan ID, intent classifier, and action decider: lines 4219-4320.
## Phase 6 - Execution, Refinement, and Block Context
- [ ] Migrate `executePlanFromCard`: lines 4321-4768.
- [ ] Migrate stop execution: lines 4769-4794.
- [ ] Migrate clear chat context: lines 4795-4870.
- [ ] Migrate serialized block creation: lines 4871-4953.
- [ ] Migrate block reformatting: lines 4954-5056.
- [ ] Migrate plan revision: lines 5057-5142.
- [ ] Migrate edit plan apply/cancel: lines 5143-5280.
- [ ] Migrate clarification context effect/helper: lines 5281-5327.
- [ ] Migrate duplicate heading removal: lines 5328-5362.
- [ ] Migrate refineable block/list helpers: lines 5363-5442.
- [ ] Migrate context block helpers: lines 5443-5520.
- [ ] Migrate slang/AI-slop detection helpers: lines 5521-5606.
- [ ] Migrate refinement diagnosis and block mention resolution: lines 5607-5799.
- [ ] Migrate `handleChatRefinement`: lines 5800-6388.
- [ ] Verify `wpaw-diff-added` and `wpaw-diff-removed` behavior survives unchanged.
## Phase 7 - Chat Input and Main Async Flows
- [ ] Migrate refine-all modal: lines 6389-6455.
- [ ] Migrate mention options: lines 6456-6579.
- [ ] Migrate custom insert-mention event effect: lines 6580-6612.
- [ ] Migrate input change/key handling: lines 6613-6701.
- [ ] Migrate mention and slash insertion helpers: lines 6702-6758.
- [ ] Migrate `sendMessage`: lines 6759-8056.
- [ ] Migrate `submitAnswers`: lines 8057-8476.
- [ ] Verify all stream readers, decoders, timeouts, active readers, and abort branches match the source.
## Phase 8 - Render Surfaces
- [ ] Migrate `renderClarification`: lines 8477-8829.
- [ ] Verify `renderSingleChoice`: lines 8486-8547.
- [ ] Verify `renderMultipleChoice`: lines 8550-8577.
- [ ] Verify `renderOpenText`: lines 8580-8597.
- [ ] Verify `renderConfigForm`: lines 8600-8723.
- [ ] Verify answer input switch: lines 8725-8742.
- [ ] Migrate conversation session actions: lines 8830-9004.
- [ ] Migrate `renderWelcomeScreen`: lines 9005-9184.
- [ ] Migrate `renderWritingEmptyState`: lines 9185-9253.
- [ ] Migrate `renderFocusKeywordBar`: lines 9254-9459.
- [ ] Migrate `renderAgentWorkspaceCard`: lines 9460-9629.
- [ ] Preserve `renderContextIndicator = renderAgentWorkspaceCard`: lines 9630-9632.
- [ ] Migrate `renderContextualAction`: lines 9633-9996.
- [ ] Migrate `renderMessages`: lines 9997-10917.
- [ ] Migrate `renderConfigTab`: lines 10918-11434.
- [ ] Migrate `getAgentStatus`: lines 11435-11448.
- [ ] Migrate `renderGlobalStatusBar`: lines 11449-11585.
- [ ] Migrate `renderChatTab`: lines 11586-12020.
- [ ] Migrate cost state/effect/helpers: lines 12021-12057.
- [ ] Migrate `renderCostTab`: lines 12058-12306.
- [ ] Migrate main component return tree: lines 12307-12346.
## Phase 9 - Render Surface Element Audit
- [ ] Refine modal: confirm dialog role, aria modal/label, overlay, modal, title, body, checkbox, cancel, continue.
- [ ] Clarification UI: confirm all question types, custom answer field, config fields, progress bar, previous/skip/next/finish behavior.
- [ ] Welcome UI: confirm recent session, older session details, open/delete buttons, keyword input, mode pills, start button.
- [ ] Writing empty state: confirm SVG icon, copy, create outline button, hint text.
- [ ] Focus keyword bar: confirm expanded/compact branches, suggestions, selected state, provider/cost display, expand/collapse.
- [ ] Workspace card: confirm collapsed state, status, context grid, keyword field, conversation/provider summary, resume card.
- [ ] Contextual action: confirm `create_outline` action, clarity check branch, generate-plan stream branch, action card.
- [ ] Messages: confirm markdown helpers, grouping, timeline, plan, edit plan, structured errors, retry buttons, default response, resume actions.
- [ ] Config tab: confirm every section, control, description, SEO controls, meta generation, audit score, check list, fix buttons.
- [ ] Status bar: confirm status dot, memory badge, undo, sessions, chat, workspace toggle, config, cost buttons.
- [ ] Chat tab: confirm lock banners, health notices, welcome/empty/workspace/activity gates, command area, hint, textarea, autocompletes, search toggle, stop/send, keyboard hints, modal.
- [ ] Cost tab: confirm header, refresh, cards, budget section, warning, action summary table, history table, footer link.
- [ ] Final shell: confirm menu item, sidebar title, icon, panel, tab wrapper, active tab branch.
## Phase 10 - Class Inventory Audit
- [ ] Verify every literal `className` token from `SIDEBAR_1_TO_1_MIGRATION.md` exists in the migrated target.
- [ ] Verify dynamic classes remain dynamic expressions.
- [ ] Verify `wpaw-diff-removed` append branch: lines 6075-6078 and 6111-6114.
- [ ] Verify `wpaw-diff-added` append branches: lines 6085-6089 and 6128-6129.
- [ ] Verify welcome pill `active` branches: lines 9152-9154 and 9162-9164.
- [ ] Verify focus suggestion `selected`: lines 9322-9324.
- [ ] Verify workspace `is-collapsed` and `status-${activeWorkspaceStatus}`: lines 9489 and 9514.
- [ ] Verify timeline `statusClass` and `is-current`: lines 10351-10375.
- [ ] Verify plan section `section.status || "pending"`: line 10582.
- [ ] Verify SEO/meta status classes: lines 11165-11374.
- [ ] Verify status-bar `is-active` branches: lines 11510-11572.
- [ ] Verify chat input classes: lines 11596-11955.
- [ ] Verify budget status classes: lines 12129, 12158, and 12167.
## Phase 11 - Non-Render Class and Selector Audit
- [ ] Preserve WordPress lock key `wpaw-writing`: lines 824 and 828.
- [ ] Preserve body class `wpaw-editor-locked`: lines 825 and 829.
- [ ] Preserve WordPress lock key `wpaw-refining`: lines 835 and 838.
- [ ] Preserve body class `wpaw-refining-locked`: lines 836 and 839.
- [ ] Preserve editor block class `wpaw-block-refining`: lines 895, 903, and 916.
- [ ] Preserve input-blocking selector `.wpaw-sidebar, .wpaw-command-area, .wpaw-messages`: line 933.
- [ ] Preserve diff cleanup removal of `wpaw-diff-removed`: line 5262.
## Phase 12 - Endpoint and Integration Audit
- [ ] Verify `/post-config/${postId}` calls.
- [ ] Verify `/cost-tracking/${postId}` calls.
- [ ] Verify `/writing-state/${postId}` calls.
- [ ] Verify `/seo-audit/${postId}` call.
- [ ] Verify `/generate-meta` call.
- [ ] Verify all conversation endpoints.
- [ ] Verify `/conversation/${postId}` and `/conversation/${data.post_id}` calls.
- [ ] Verify `/chat-history/${postId}` call.
- [ ] Verify `/memanto/restore?post_id=${postId}` call.
- [ ] Verify `/refine-title` call.
- [ ] Verify all `/generate-plan` calls.
- [ ] Verify all `/chat` calls.
- [ ] Verify `/section-blocks/${postId}` and `/section-blocks` calls.
- [ ] Verify `/summarize-context` call.
- [ ] Verify `/detect-intent` call.
- [ ] Verify `/clear-context` call.
- [ ] Verify `/suggest-keywords` call.
- [ ] Verify `/execute-article` call.
- [ ] Verify `/reformat-blocks` call.
- [ ] Verify `/revise-plan` call.
- [ ] Verify `/refine-from-chat` call.
- [ ] Verify `/check-clarity` calls.
- [ ] For every endpoint, verify method, nonce header, content type, body shape, stream/non-stream handling, provider metadata, cost updates, and error handling.
## Phase 13 - Gutenberg and Browser Side Effects Audit
- [ ] Verify all `select("core/block-editor")` calls.
- [ ] Verify all `dispatch("core/block-editor")` calls.
- [ ] Verify all `select("core/editor")` calls.
- [ ] Verify all `dispatch("core/editor")` calls.
- [ ] Verify `wp.data.subscribe` behavior.
- [ ] Verify `wp.data.withSelect` behavior.
- [ ] Verify localStorage keys: `wpaw_agent_workspace_collapsed` and `wpawSessionId_${postId}`.
- [ ] Verify beforeunload handlers.
- [ ] Verify global document event listeners and cleanups.
- [ ] Verify window `wpaw:insert-mention` listener and cleanup.
- [ ] Verify lock heartbeat interval setup and cleanup.
- [ ] Verify debounced config/message/keyword saves.
- [ ] Verify `AbortController` lifecycle.
- [ ] Verify stream reader lifecycle and active reader cancellation.
- [ ] Verify `TextDecoder` usage.
## Phase 14 - Final Parity Audit
- [ ] Confirm no functions are missing from `SIDEBAR_1_TO_1_MIGRATION.md`.
- [ ] Confirm no state/ref/effect entries are missing.
- [ ] Confirm no render surface entries are missing.
- [ ] Confirm no class inventory entries are missing.
- [ ] Confirm no dynamic class expressions were flattened.
- [ ] Confirm no endpoint inventory entries are missing.
- [ ] Confirm no editor/browser side effects are missing.
- [ ] Confirm final plugin registration still renders `ConnectedSidebar`.
- [ ] Confirm no optional extraction was performed before monolith parity.
- [ ] Confirm any future extraction task references the source line range it moves.
## Optional Phase 15 - Extraction After Monolith Parity Only
- [ ] Extract only one helper group at a time.
- [ ] Before extraction, record source range and destination file.
- [ ] Move code without renaming public/internal symbols.
- [ ] Preserve imports/dependencies exactly.
- [ ] Re-run the same parity checks for the moved range.
- [ ] Do not extract render surfaces until all behavior helpers are proven equivalent.
- [ ] Do not delete the monolith source range until the moved range is checked line-by-line.
## Stop Conditions
- [ ] Stop if `sidebar.js` changes and this tasklist has not been regenerated.
- [ ] Stop if a migrated range requires guessing.
- [ ] Stop if a function seems unused but exists in `sidebar.js`.
- [ ] Stop if a class appears styling-only but exists in `sidebar.js`.
- [ ] Stop if an endpoint or request body differs from `sidebar.js`.
- [ ] Stop if hook order would change.

View File

@@ -66,7 +66,9 @@
color: #a7aaad;
text-transform: uppercase;
letter-spacing: 0.05em;
transition: color 0.1s ease, border-color 0.1s ease;
transition:
color 0.1s ease,
border-color 0.1s ease;
margin-bottom: -1px;
}
@@ -176,6 +178,188 @@
background: #525b6b;
}
.wpaw-agent-workspace-card {
background: linear-gradient(135deg, #111827 0%, #1e293b 100%);
border: 1px solid #334155;
border-radius: 12px;
margin: 10px 10px 8px;
padding: 12px;
color: #e5e7eb;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.28);
}
.wpaw-agent-workspace-card.is-collapsed {
padding: 9px 10px;
}
.wpaw-agent-workspace-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.wpaw-agent-workspace-card.is-collapsed .wpaw-agent-workspace-header {
align-items: center;
margin-bottom: 0;
}
.wpaw-agent-workspace-heading {
min-width: 0;
}
.wpaw-agent-workspace-kicker {
color: #93c5fd;
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 700;
margin-bottom: 3px;
}
.wpaw-agent-workspace-title {
color: #f8fafc;
font-size: 13px;
font-weight: 700;
line-height: 1.3;
}
.wpaw-agent-workspace-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.wpaw-agent-workspace-status {
white-space: nowrap;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.35);
color: #cbd5e1;
background: rgba(15, 23, 42, 0.7);
padding: 4px 8px;
font-size: 10px;
text-transform: capitalize;
}
.wpaw-agent-workspace-toggle {
border: 1px solid rgba(147, 197, 253, 0.35);
border-radius: 999px;
background: rgba(15, 23, 42, 0.42);
color: #bfdbfe;
cursor: pointer;
font-size: 10px;
font-weight: 700;
line-height: 1;
padding: 5px 8px;
}
.wpaw-agent-workspace-toggle:hover,
.wpaw-agent-workspace-toggle:focus {
border-color: #60a5fa;
color: #eff6ff;
outline: none;
}
.wpaw-agent-workspace-status.status-in_progress,
.wpaw-agent-workspace-status.status-paused,
.wpaw-agent-workspace-status.status-running,
.wpaw-agent-workspace-status.status-stopping {
color: #fbbf24;
border-color: rgba(251, 191, 36, 0.45);
background: rgba(113, 63, 18, 0.28);
}
.wpaw-agent-workspace-status.status-completed {
color: #86efac;
border-color: rgba(134, 239, 172, 0.45);
background: rgba(20, 83, 45, 0.28);
}
.wpaw-agent-workspace-status.status-failed {
color: #fca5a5;
border-color: rgba(248, 113, 113, 0.45);
background: rgba(127, 29, 29, 0.28);
}
.wpaw-agent-context-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.wpaw-agent-context-item {
min-width: 0;
background: rgba(15, 23, 42, 0.52);
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 9px;
padding: 8px;
}
.wpaw-agent-context-item span {
display: block;
color: #94a3b8;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 4px;
}
.wpaw-agent-context-item strong {
display: block;
color: #f8fafc;
font-size: 12px;
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
}
.wpaw-agent-keyword-input {
width: 100%;
min-height: 26px;
border: 1px solid rgba(147, 197, 253, 0.32);
border-radius: 7px;
background: rgba(15, 23, 42, 0.78);
color: #f8fafc;
font-size: 12px;
padding: 4px 7px;
}
.wpaw-agent-keyword-input:focus {
outline: none;
border-color: #60a5fa;
box-shadow: 0 0 0 1px rgba(96, 165, 250, 0.35);
}
.wpaw-agent-resume-card {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-top: 10px;
padding: 9px;
border-radius: 9px;
background: rgba(59, 130, 246, 0.12);
border: 1px solid rgba(96, 165, 250, 0.28);
}
.wpaw-agent-resume-card strong,
.wpaw-agent-resume-card span {
display: block;
}
.wpaw-agent-resume-card strong {
color: #bfdbfe;
font-size: 12px;
}
.wpaw-agent-resume-card span {
color: #93c5fd;
font-size: 11px;
margin-top: 2px;
}
.wpaw-input-area {
background: #1e2128;
padding: 12px;
@@ -581,7 +765,7 @@ input.wpaw-plan-section-check:checked::before {
}
.wpaw-block-refining::before {
content: 'REFINING';
content: "REFINING";
position: absolute;
top: -12px;
right: 8px;
@@ -618,7 +802,7 @@ input.wpaw-plan-section-check:checked::before {
padding: 12px;
border-radius: 8px;
overflow-x: auto;
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
border: 1px solid #2d3139;
color: #c8cdd5;
@@ -629,16 +813,18 @@ input.wpaw-plan-section-check:checked::before {
color: #a5d6ff;
padding: 2px 5px;
border-radius: 4px;
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
}
.wpaw-editor-locked .admin-ui-navigable-region.interface-interface-skeleton__content {
.wpaw-editor-locked
.admin-ui-navigable-region.interface-interface-skeleton__content {
position: relative;
}
.wpaw-editor-locked .admin-ui-navigable-region.interface-interface-skeleton__content::after {
content: '';
.wpaw-editor-locked
.admin-ui-navigable-region.interface-interface-skeleton__content::after {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.55);
@@ -894,7 +1080,7 @@ input.wpaw-plan-section-check:checked::before {
}
.wpaw-ai-response .wpaw-response::before {
content: '';
content: "";
position: absolute;
left: -15px;
top: 20px;
@@ -913,7 +1099,7 @@ input.wpaw-plan-section-check:checked::before {
}
.wpaw-streaming-indicator::after {
content: '...';
content: "...";
display: inline-block;
width: 18px;
animation: wpaw-ellipsis 1.1s infinite;
@@ -951,19 +1137,19 @@ input.wpaw-plan-section-check:checked::before {
@keyframes wpaw-ellipsis {
0% {
content: '.';
content: ".";
}
33% {
content: '..';
content: "..";
}
66% {
content: '...';
content: "...";
}
100% {
content: '.';
content: ".";
}
}
@@ -1073,7 +1259,7 @@ input.wpaw-plan-section-check:checked::before {
border-color: #e2e8f0;
}
.wpaw-plan-section-row input[type=checkbox] {
.wpaw-plan-section-row input[type="checkbox"] {
transform: translateY(3px);
}
@@ -1093,7 +1279,8 @@ input.wpaw-plan-section-check:checked::before {
.wpaw-timeline-content {
flex: 1;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
color: #c8cdd5;
}
@@ -1181,7 +1368,7 @@ input.wpaw-plan-section-check:checked::before {
z-index: 2;
}
.wpaw-config-tab>*:nth-child(2) {
.wpaw-config-tab > *:nth-child(2) {
margin-top: 60px;
}
@@ -1293,14 +1480,16 @@ input.wpaw-plan-section-check:checked::before {
margin-bottom: 10px;
}
.wpaw-budget-bar~.description {
.wpaw-budget-bar ~ .description {
padding: 0 12px;
}
.wpaw-budget-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #66bb6a);
transition: width 0.5s ease, background 0.3s ease;
transition:
width 0.5s ease,
background 0.3s ease;
}
.wpaw-budget-fill.warning {
@@ -1602,7 +1791,7 @@ input.wpaw-plan-section-check:checked::before {
font-size: 13px;
}
.wpaw-previous-answers>div:last-child .wpaw-answer-text {
.wpaw-previous-answers > div:last-child .wpaw-answer-text {
margin-bottom: 0;
}
@@ -1722,11 +1911,11 @@ input.wpaw-plan-section-check:checked::before {
transition: 0.3s;
}
.wpaw-config-toggle input:checked+.wpaw-toggle-slider {
.wpaw-config-toggle input:checked + .wpaw-toggle-slider {
background-color: #2271b1;
}
.wpaw-config-toggle input:checked+.wpaw-toggle-slider:before {
.wpaw-config-toggle input:checked + .wpaw-toggle-slider:before {
transform: translateX(24px);
}
@@ -1755,11 +1944,14 @@ input.wpaw-plan-section-check:checked::before {
display: flex;
}
.wpaw-question-card .wpaw-config-form .wpaw-config-label .wpaw-config-description {
.wpaw-question-card
.wpaw-config-form
.wpaw-config-label
.wpaw-config-description {
font-size: 11px;
}
.wpaw-question-card .wpaw-config-field:has(input[type=text]) {
.wpaw-question-card .wpaw-config-field:has(input[type="text"]) {
flex-direction: column;
}
@@ -1786,7 +1978,7 @@ input.wpaw-plan-section-check:checked::before {
padding-right: 20px;
}
.wpaw-question-card .wpaw-config-form .wpaw-config-field input[type=text] {
.wpaw-question-card .wpaw-config-form .wpaw-config-field input[type="text"] {
background-color: #1a1a1a !important;
}
@@ -1812,13 +2004,12 @@ input.wpaw-plan-section-check:checked::before {
}
.dark-theme .wpaw-question-card textarea::placeholder {
color: #6c6c6c
color: #6c6c6c;
}
.dark-theme .wpaw-question-card textarea::focus,
.dark-theme .wpaw-question-card textarea::active {
border-color: #252830 !important;
;
}
/* ===========================
@@ -1861,7 +2052,6 @@ input.wpaw-plan-section-check:checked::before {
}
@keyframes wpaw-pulse {
0%,
100% {
box-shadow: 0 0 0 0px rgba(34, 113, 177, 0.2);
@@ -1920,8 +2110,9 @@ input.wpaw-plan-section-check:checked::before {
}
@media (max-width: 482px) {
.interface-complementary-area__fill:has(#wp-agentic-writer\:wp-agentic-writer),
.interface-complementary-area__fill:has(
#wp-agentic-writer\:wp-agentic-writer
),
#wp-agentic-writer\:wp-agentic-writer {
width: 100vw !important;
}
@@ -1933,7 +2124,9 @@ input.wpaw-plan-section-check:checked::before {
height: 6px;
border-radius: 50%;
background-color: #3b82f6;
box-shadow: 12px 0 #3b82f6, -12px 0 #3b82f6;
box-shadow:
12px 0 #3b82f6,
-12px 0 #3b82f6;
position: relative;
animation: wpaw-flash 0.5s ease-out infinite alternate;
margin: 0 20px 0 16px;
@@ -1943,17 +2136,23 @@ input.wpaw-plan-section-check:checked::before {
@keyframes wpaw-flash {
0% {
background-color: #93c5fd;
box-shadow: 12px 0 #93c5fd, -12px 0 #3b82f6;
box-shadow:
12px 0 #93c5fd,
-12px 0 #3b82f6;
}
50% {
background-color: #3b82f6;
box-shadow: 12px 0 #93c5fd, -12px 0 #93c5fd;
box-shadow:
12px 0 #93c5fd,
-12px 0 #93c5fd;
}
100% {
background-color: #93c5fd;
box-shadow: 12px 0 #3b82f6, -12px 0 #93c5fd;
box-shadow:
12px 0 #3b82f6,
-12px 0 #93c5fd;
}
}
@@ -1977,7 +2176,8 @@ input.wpaw-plan-section-check:checked::before {
padding: 8px 12px;
background: #1d2227;
color: #fff;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-family:
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
border-bottom: 1px solid #3c3c3c;
}
@@ -2004,11 +2204,22 @@ input.wpaw-plan-section-check:checked::before {
animation: statusPulse 1s infinite;
}
.wpaw-status-dot.checking,
.wpaw-status-dot.refining {
background: #60a5fa;
animation: statusPulse 0.8s infinite;
}
.wpaw-status-dot.writing {
background: #2271b1;
animation: statusPulse 0.8s infinite;
}
.wpaw-status-dot.stopping {
background: #f97316;
animation: statusPulse 0.55s infinite;
}
.wpaw-status-dot.complete {
background: #00a32a;
}
@@ -2018,7 +2229,6 @@ input.wpaw-plan-section-check:checked::before {
}
@keyframes statusPulse {
0%,
100% {
opacity: 1;
@@ -2035,6 +2245,22 @@ input.wpaw-plan-section-check:checked::before {
font-weight: 500;
}
.wpaw-memanto-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
background: rgba(99, 155, 255, 0.15);
color: #93b8ff;
border: 1px solid rgba(99, 155, 255, 0.25);
cursor: default;
white-space: nowrap;
}
.wpaw-status-cost {
color: #a7aaad;
font-size: 11px;
@@ -2047,7 +2273,8 @@ input.wpaw-plan-section-check:checked::before {
flex-direction: column;
overflow-y: auto;
background: #fff;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-family:
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 13px;
line-height: 1.5;
}
@@ -2127,7 +2354,9 @@ input.wpaw-plan-section-check:checked::before {
/* Agent Response (prose) */
.wpaw-log-entry.agent-response {
border-left-color: #dcdcde;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans,
Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
/* Command Input Area */
@@ -2287,7 +2516,9 @@ input.wpaw-plan-section-check:checked::before {
font-size: 14px;
padding: 2px;
line-height: 1;
transition: color 0.1s ease, transform 0.1s ease;
transition:
color 0.1s ease,
transform 0.1s ease;
}
.wpaw-status-icon-btn:hover {
@@ -2391,11 +2622,11 @@ input.wpaw-plan-section-check:checked::before {
transition: opacity 0.15s ease;
}
.wpaw-web-search-toggle input:checked+.wpaw-web-search-icon {
.wpaw-web-search-toggle input:checked + .wpaw-web-search-icon {
opacity: 1;
}
.wpaw-web-search-toggle input:checked+.wpaw-web-search-icon * {
.wpaw-web-search-toggle input:checked + .wpaw-web-search-icon * {
stroke: #4caf50;
}
@@ -2408,7 +2639,7 @@ input.wpaw-plan-section-check:checked::before {
transition: color 0.15s ease;
}
.wpaw-web-search-toggle input:checked~.wpaw-web-search-label {
.wpaw-web-search-toggle input:checked ~ .wpaw-web-search-label {
color: #4caf50;
}
@@ -2513,6 +2744,17 @@ input.wpaw-plan-section-check:checked::before {
transform: scale(1.05);
}
.wpaw-stop-circle-btn.is-stopping,
.wpaw-stop-circle-btn.is-stopping:hover {
background: #f97316;
cursor: wait;
transform: none;
}
.wpaw-stop-spinner {
animation: wpaw-spin 0.85s linear infinite;
}
.wpaw-command-circle-btn svg {
width: 20px !important;
height: 20px !important;
@@ -2827,10 +3069,37 @@ input.wpaw-plan-section-check:checked::before {
}
.wpaw-seo-check .check-label {
flex: 1;
color: #a7aaad;
min-width: 0;
}
.wpaw-meta-info>button.components-button.is-secondary.is-small {
.wpaw-seo-fix-button.components-button.is-secondary.is-small {
border-color: rgba(96, 165, 250, 0.72);
box-shadow: none !important;
color: #bfdbfe;
flex-shrink: 0;
height: 24px;
min-width: 44px;
padding: 0 8px;
}
.wpaw-seo-fix-button.components-button.is-secondary.is-small:hover:not(
:disabled
),
.wpaw-seo-fix-button.components-button.is-secondary.is-small:focus:not(
:disabled
) {
border-color: #60a5fa;
color: #eff6ff;
}
.wpaw-seo-fix-button.components-button.is-secondary.is-small.is-fixing {
border-color: #fbbf24;
color: #fde68a;
}
.wpaw-meta-info > button.components-button.is-secondary.is-small {
outline: unset !important;
color: #fbbf24;
border: 1px solid #fbbf24;
@@ -3368,7 +3637,9 @@ input.wpaw-plan-section-check:checked::before {
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
transition:
border-color 0.2s,
background 0.2s;
}
.wpaw-fk-select {
@@ -3386,7 +3657,9 @@ input.wpaw-plan-section-check:checked::before {
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
transition: border-color 0.2s, background 0.2s;
transition:
border-color 0.2s,
background 0.2s;
}
.wpaw-fk-input:focus {
@@ -3411,7 +3684,9 @@ input.wpaw-plan-section-check:checked::before {
padding: 10px 12px;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s, background 0.2s;
transition:
border-color 0.2s,
background 0.2s;
}
.wpaw-fk-custom-input:focus {
@@ -3460,7 +3735,9 @@ input.wpaw-plan-section-check:checked::before {
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
transition:
background 0.2s,
color 0.2s;
}
.wpaw-fk-expand:hover,
@@ -3618,7 +3895,9 @@ input.wpaw-plan-section-check:checked::before {
font-size: 14px;
margin-bottom: 1rem;
box-sizing: border-box;
transition: border-color 0.2s, background 0.2s;
transition:
border-color 0.2s,
background 0.2s;
}
.wpaw-welcome-input:focus {
@@ -3919,7 +4198,6 @@ input.wpaw-plan-section-check:checked::before {
P2: TYPING ANIMATION
=========================== */
@keyframes wpaw-typewriter-cursor {
0%,
100% {
border-color: transparent;
@@ -3962,7 +4240,6 @@ input.wpaw-plan-section-check:checked::before {
}
@keyframes wpaw-typing-bounce {
0%,
60%,
100% {
@@ -4030,8 +4307,13 @@ input.wpaw-plan-section-check:checked::before {
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.wpaw-suggestion-item {
@@ -4169,7 +4451,9 @@ input.wpaw-plan-section-check:checked::before {
max-width: 90vw;
background: #2d2d2d;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.1);
overflow: hidden;
animation: wpaw-palette-slide-in 0.15s ease-out;
}
@@ -4550,7 +4834,6 @@ input.wpaw-plan-section-check:checked::before {
color: #f59e0b;
}
/* ===========================
AUDIT FIXES: Mode Indicator Badge
=========================== */
@@ -4567,10 +4850,20 @@ input.wpaw-plan-section-check:checked::before {
margin-bottom: 0.5em !important;
letter-spacing: normal !important;
}
.wpaw-response-content h1 {
font-size: 20px !important;
color: #e8ecf2 !important;
font-weight: bold;
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.wpaw-response-content h2 {
font-size: 17px !important;
color: #e8ecf2 !important;
font-weight: bold;
margin-top: 1rem;
margin-bottom: 1rem;
}
.wpaw-response-content h4,
@@ -4580,6 +4873,17 @@ input.wpaw-plan-section-check:checked::before {
color: #d0d5dd !important;
}
.wpaw-response-content table {
border-collapse: collapse;
width: 100%;
}
.wpaw-response-content table th,
table td {
border: 1px solid #dce0e8 !important;
padding: 4px 6px;
}
.wpaw-mode-badge {
display: inline-flex;
align-items: center;
@@ -4763,7 +5067,7 @@ input.wpaw-plan-section-check:checked::before {
color: #dce0e8;
}
.wpaw-response-content>* {
.wpaw-response-content > * {
padding: 1rem;
}
@@ -4801,7 +5105,8 @@ input.wpaw-plan-section-check:checked::before {
.wpaw-config-summary-item {
color: #9aa5b4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
margin-bottom: 4px;
font-size: 11.5px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,435 @@
# MEMANTO Integration Plan — Optional Context Enhancement
**Version:** 1.0
**Date:** 2026-06-07
**Status:** Planning
**Depends on:** MEMANTO_PRICING_STRATEGY.md
---
## Design Principles
1. **MEMANTO is optional.** The plugin's built-in Context Builder (`class-context-builder.php`) remains the default and always works without MEMANTO.
2. **MEMANTO enhances, never replaces.** MySQL sessions (`wpaw_conversations`) remain the primary session store. MEMANTO runs parallel.
3. **Zero disruption on failure.** If MEMANTO is unreachable, the plugin falls back to existing behavior with no error shown to the user.
4. **Server-side only.** All MEMANTO API calls happen in PHP. The frontend (sidebar.js) is unaware of MEMANTO — it just sees richer or leaner context in AI responses.
5. **User brings own Moorcheh key.** Plugin stores MEMANTO URL + Moorcheh API key in WordPress settings. Never hardcoded.
---
## Architecture Overview
```
┌────────────────────────────────────────────────┐
│ WordPress Backend │
│ │
User Message ──────► │ Gutenberg Sidebar (handle_chat_request) │
│ │ │
│ ▼ │
│ Context Builder (build_system_message) │
│ │ │
│ ├──► MySQL Session (wpaw_conversations) │
│ │ primary store │
│ │ │
│ ├──► Memanto Client ──► MEMANTO API │
│ │ (if configured) │
│ │ │ │
│ │ ├── recall (retrieve) │
│ │ └── remember (store) │
│ │ │
│ ▼ │
│ Merged Context → AI Provider → Response │
└────────────────────────────────────────────────┘
```
### Data Flow: Two Paths
| Path | When | What Happens |
|---|---|---|
| **Default (no MEMANTO)** | MEMANTO URL not configured in settings | Context Builder uses MySQL session only. Identical to current behavior. |
| **MEMANTO active** | MEMANTO URL + Moorcheh key configured and validated | Context Builder queries MEMANTO for relevant memories before building context. After AI response, significant events are stored in MEMANTO. |
---
## Agent Design
### Agent Naming Convention
| Agent ID | Scope | Purpose |
|---|---|---|
| `wp-user-{wordpress_user_id}` | Per WordPress user | Cross-post preferences: writing style, tone, audience, language, brand voice |
| `wp-post-{wordpress_post_id}` | Per post | Article-specific: plan decisions, rejections, research, section progress |
### Memory Types Used
| MEMANTO Type | When Stored | Example |
|---|---|---|
| `preference` | User sets/changes post config | "User prefers conversational tone, intermediate audience" |
| `instruction` | User sends chat message | "Focus on plugin vulnerabilities only" |
| `decision` | User approves/rejects plan | "Approved 5-section outline for WordPress security" |
| `artifact` | Plan generated, section written | "Plan: 5 sections covering X, Y, Z" |
| `context` | Session ends / summarize | "Article at 60% completion, 3 of 5 sections done" |
| `error` | User corrects AI output | "User rejected generic tips approach, wants specific plugin recommendations" |
### Tags Convention
Tags enable targeted recall. Every memory includes:
| Tag | Example | Purpose |
|---|---|---|
| `post:{id}` | `post:42` | Scope recall to specific post |
| `site:{domain}` | `site:example.com` | Scope to WordPress site |
| `mode:{mode}` | `mode:planning` | What mode was active |
| `model:{model}` | `model:deepseek-chat` | Which model was used |
---
## New Files to Create
### `includes/class-memanto-client.php`
PHP client for MEMANTO API v2. Singleton class.
**Public Methods:**
| Method | Description |
|---|---|
| `is_configured()` | Returns true if MEMANTO URL + Moorcheh key are set in settings |
| `is_healthy()` | Calls `/health` endpoint, caches result for 5 minutes |
| `ensure_agent( $agent_id )` | Creates agent via `POST /api/v2/agents` if not exists |
| `activate_session( $agent_id )` | `POST /api/v2/agents/{id}/activate`, caches session token in transient |
| `remember( $agent_id, $content, $type, $tags, $title )` | `POST /api/v2/agents/{id}/remember` |
| `batch_remember( $agent_id, $memories )` | `POST /api/v2/agents/{id}/batch-remember` |
| `recall( $agent_id, $query, $type, $limit )` | `POST /api/v2/agents/{id}/recall` |
| `recall_recent( $agent_id, $limit )` | `POST /api/v2/agents/{id}/recall/recent` |
| `deactivate_session( $agent_id )` | `POST /api/v2/agents/{id}/deactivate` |
**Internal Mechanics:**
- Session token stored in WP transient: `wpaw_memanto_token_{agent_id}` (6-hour TTL matching MEMANTO JWT)
- Auto-reactivates on expired token (catches 401, re-activates, retries)
- All calls use `wp_remote_post` / `wp_remote_get` with 10-second timeout
- All calls wrapped in try/catch with `wpaw_debug_log` on failure
- Moorcheh API key passed via `X-API-Key` header or configured in MEMANTO instance (depending on MEMANTO's auth model)
### `includes/class-memanto-context-enhancer.php`
Orchestrates when and what to remember/recall. Hooks into existing Context Service.
**Public Methods:**
| Method | Hook Point | Description |
|---|---|---|
| `on_session_start( $session_id, $post_id, $user_id )` | Session creation | Ensures user + post agents exist; recalls previous session state |
| `on_user_message( $session_id, $content, $post_id )` | After user sends message | Stores instruction-type memory |
| `on_plan_generated( $post_id, $plan )` | After plan creation | Stores artifact-type memory |
| `on_plan_approved( $post_id, $plan )` | User approves plan | Stores decision-type memory |
| `on_plan_rejected( $post_id, $reason )` | User rejects/requests changes | Stores error-type memory with rejection reason |
| `on_section_written( $post_id, $section_id, $summary )` | After section generation | Stores artifact-type memory |
| `on_block_refined( $post_id, $block_id, $instruction )` | After refinement | Stores instruction-type memory |
| `on_config_saved( $post_id, $config )` | Post config updated | Stores preference-type memory to both user and post agents |
| `on_session_end( $session_id, $post_id )` | Session completed/archived | Summarizes session, stores context-type memory, deactivates session |
| `recall_for_context( $post_id, $user_id, $current_message )` | Before building context | Returns recalled memories to enrich prompt |
**Recall Strategy (`recall_for_context`):**
1. Recall recent memories from post agent (limit: 10)
2. Recall semantically relevant memories from post agent (query: user's current message, limit: 5)
3. Recall user preferences from user agent (query: "writing preferences tone audience", limit: 5)
4. Deduplicate by content hash
5. Return structured array of recalled items
---
## Files to Modify
### `includes/class-settings-v2.php`
Add MEMANTO configuration section:
```
MEMANTO Context Keeper
├── Enable MEMANTO integration (checkbox, default: off)
├── MEMANTO Instance URL (text, e.g., https://abc123.context.wpagentic.dev)
├── Moorcheh API Key (password, user's own key)
└── Connection Status (read-only, shows "Connected" / "Not configured" / "Error: ...")
```
Add a "Test Connection" button that calls MEMANTO `/health` endpoint.
### `includes/class-context-builder.php`
Modify `build_for_task()` method. After line ~52 where `$saved_context` is loaded:
```php
// Existing: MySQL context
$saved_context = $context_service->get_context( $session_id, $post_id );
// NEW: MEMANTO enhancement (if configured)
$memanto_context = array();
$memanto_client = WP_Agentic_Writer_Memanto_Client::get_instance();
if ( $memanto_client->is_configured() && $memanto_client->is_healthy() ) {
$enhancer = WP_Agentic_Writer_Memanto_Context_Enhancer::get_instance();
$memanto_context = $enhancer->recall_for_context(
$post_id,
get_current_user_id(),
$request_params['latestUserMessage'] ?? ''
);
}
```
Modify `build_working_context()` to include a new section:
```php
// After existing sections, before "Recent saved conversation excerpts"
if ( ! empty( $memanto_context ) ) {
$memory_lines = $this->format_memanto_memories( $memanto_context );
if ( '' !== $memory_lines ) {
$sections[] = "PERSISTENT MEMORY (recalled from MEMANTO):\n" . $memory_lines;
}
}
```
**Key rule:** MEMANTO context is **additive**. It never replaces the existing `BACKEND CONTINUITY CONTEXT` section. It supplements it.
### `includes/class-context-service.php`
Add MEMANTO write-through hooks in key methods:
| Method | Hook Added |
|---|---|
| `save_plan()` | `$enhancer->on_plan_generated( $post_id, $plan )` |
| `update_session_context()` | `$enhancer->on_config_saved()` if config changed |
| `add_message()` | `$enhancer->on_user_message()` for user-role messages |
| `clear_context()` | Optionally clear MEMANTO post agent memories |
### `includes/class-gutenberg-sidebar.php`
Add MEMANTO hooks in key handler methods:
| Handler | Hook Added |
|---|---|
| `handle_chat_request()` | `$enhancer->on_user_message()` after saving to MySQL |
| `handle_generate_plan()` | `$enhancer->on_plan_generated()` after successful plan |
| `handle_execute_article()` | `$enhancer->on_section_written()` per section |
| `handle_refine_block()` | `$enhancer->on_block_refined()` |
| `handle_summarize_context()` | Skip AI call if MEMANTO active — return cached recall instead |
| `handle_detect_intent()` | Skip AI call if MEMANTO active — use regex + MEMANTO context instead |
Add new REST endpoint:
```
POST /wp-agentic-writer/v1/memanto/status
→ Returns: { connected: bool, agent_count: int, memory_count: int, last_recall: string }
```
### `includes/class-autoloader.php`
Register the two new classes:
- `class-memanto-client.php``WP_Agentic_Writer_Memanto_Client`
- `class-memanto-context-enhancer.php``WP_Agentic_Writer_Memanto_Context_Enhancer`
### `assets/js/sidebar.js` (minimal change)
No MEMANTO-specific logic needed. Optional enhancement:
- Show a small "🧠 Memory active" indicator when MEMANTO is connected
- Show "memories recalled: N" in the context audit display
---
## Graceful Degradation Strategy
```
MEMANTO call succeeds?
├── YES → Merge MEMANTO context into working context
└── NO
├── MEMANTO not configured → Use MySQL-only context (default behavior)
├── MEMANTO timeout (>10s) → Log warning, use MySQL-only context
├── MEMANTO 401 (token expired) → Re-activate session, retry once, then fallback
└── MEMANTO 5xx (server error) → Log error, use MySQL-only context
```
User **never** sees an error from MEMANTO. The worst case is they get the same experience as users without MEMANTO.
---
## Implementation Phases
### Phase 1: Core Client (Week 1)
**Goal:** MEMANTO client class + settings UI + connection validation
| Task | File | Details |
|---|---|---|
| Create Memanto Client | `class-memanto-client.php` | All API methods, session token management, error handling |
| Create Context Enhancer shell | `class-memanto-context-enhancer.php` | Skeleton with `is_configured()` check on every method |
| Add settings section | `class-settings-v2.php` | URL field, API key field, enable checkbox, test button |
| Register in autoloader | `class-autoloader.php` | Add both new classes |
| Add REST status endpoint | `class-gutenberg-sidebar.php` | `/memanto/status` endpoint |
**Validation:** Admin can configure MEMANTO URL + Moorcheh key, test connection, see "Connected" status. No functional changes to AI features yet.
### Phase 2: Write-Through Memory (Week 2)
**Goal:** Store memories on every meaningful action
| Task | Hook Point | Memory Type |
|---|---|---|
| Store on user message | `handle_chat_request()` | `instruction` |
| Store on plan generated | `handle_generate_plan()` | `artifact` |
| Store on plan approved | After plan save in frontend | `decision` |
| Store on plan rejected | Plan revision flow | `error` |
| Store on section written | `handle_execute_article()` | `artifact` |
| Store on block refined | `handle_refine_block()` | `instruction` |
| Store on config saved | `update_session_context()` | `preference` |
| Store on session end | Session completed/archived | `context` |
**Validation:** Write an article with MEMANTO enabled. Check MEMANTO API (via recall endpoint) that memories were stored. Verify plugin still works perfectly with MEMANTO disabled.
### Phase 3: Context Enrichment (Week 3)
**Goal:** Recall memories to enrich AI prompts
| Task | File | Details |
|---|---|---|
| Add `recall_for_context()` | `class-memanto-context-enhancer.php` | 3-recall strategy (recent, semantic, preferences) |
| Modify `build_for_task()` | `class-context-builder.php` | Merge recalled memories into working context |
| Add `format_memanto_memories()` | `class-context-builder.php` | Format recalled items as compact prompt text |
| Skip summarize-context when MEMANTO active | `class-gutenberg-sidebar.php` | Return cached recall instead of AI call |
| Skip detect-intent when MEMANTO active | `class-gutenberg-sidebar.php` | Use regex + MEMANTO context instead |
**Validation:** Write an article. Mid-session, close the browser. Reopen the post. Verify AI "remembers" context from recalled memories. Compare AI response quality with/without MEMANTO.
### Phase 4: Cross-Session Restore (Week 4)
**Goal:** Seamless experience when returning to a post after days/weeks
| Task | Details |
|---|---|
| Session restore on load | When post editor opens, recall recent post memories. Build a "restored session" system message. |
| Frontend indicator | Show "🧠 Restored from memory" badge in sidebar |
| User preference carry-over | On new post creation, recall user agent preferences for default post config |
| Session deactivation | On session end, call MEMANTO deactivate to trigger summary generation |
**Validation:** Create an article. Complete 50%. Wait 1 day. Open the post again. Verify AI picks up where it left off without user re-explaining context.
### Phase 5: Polish & Edge Cases (Week 5)
| Task | Details |
|---|---|
| Memory pruning | On session end, summarize verbose raw messages into compact context memories |
| Connection health UI | Real-time status indicator in plugin sidebar header |
| Moorcheh limit warning | When approaching 10K vectors, show admin notice with upgrade link |
| Error logging | Detailed MEMANTO error logging with `wpaw_debug_log` |
| Settings validation | Validate URL format, API key format, connection test before saving |
---
## Testing Strategy
### Unit Tests
| Test | Description |
|---|---|
| `test_memanto_client_not_configured` | Client returns false when settings empty |
| `test_memanto_client_health_check` | Mock `/health` response, verify caching |
| `test_memanto_client_remember` | Mock remember API, verify payload structure |
| `test_memanto_client_recall` | Mock recall API, verify response parsing |
| `test_memanto_client_session_lifecycle` | Activate → remember → recall → deactivate |
| `test_enhancer_graceful_fallback` | MEMANTO returns error, context builder still works |
| `test_context_builder_with_memanto` | Verify MEMANTO context is included in working context |
| `test_context_builder_without_memanto` | Verify no MEMANTO content when not configured |
### Integration Tests
| Test | Description |
|---|---|
| Full article with MEMANTO | Chat → Plan → Write → Refine. Verify memories stored at each step. |
| Full article without MEMANTO | Same flow. Verify no MEMANTO calls made. Plugin works identically. |
| MEMANTO goes down mid-session | Start with MEMANTO active. Simulate timeout. Verify graceful fallback. |
| Cross-session restore | Write 50% of article. Simulate new session. Verify AI context restored. |
| Multi-site with same MEMANTO | Use same MEMANTO instance across 2 sites. Verify agent isolation. |
### Manual Test Checklist
- [ ] Plugin activates with no MEMANTO settings — works normally
- [ ] MEMANTO URL set but Moorcheh key empty — shows "not configured"
- [ ] MEMANTO URL + invalid key — shows "connection error"
- [ ] MEMANTO URL + valid key — shows "connected"
- [ ] Write article with MEMANTO on — AI responses include recalled memory
- [ ] Write article with MEMANTO off — identical to current behavior
- [ ] Disable MEMANTO mid-session — no errors, fallback to MySQL-only
- [ ] Re-enable MEMANTO — picks up from where it left off
- [ ] Check MEMANTO recall endpoint — memories exist for test post
---
## Performance Considerations
| Concern | Mitigation |
|---|---|
| MEMANTO recall adds latency to every AI call | Cache recall results in transient (5-min TTL). Only recall when context builder runs. |
| Session token expires mid-request | Auto-reactivate on 401. Single retry. |
| Too many memories stored | Batch-remember to reduce HTTP calls. Summarize on session end. |
| MEMANTO instance overloaded | 10-second timeout on all calls. Graceful fallback. |
| WordPress transient cache bloat | Use specific key patterns. Clean up on session end. |
---
## Settings UI Specification
### MEMANTO Context Keeper Section
Located in WP Agentic Writer → Settings → MEMANTO tab.
```
┌─────────────────────────────────────────────────────────────────┐
│ MEMANTO Context Keeper │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ☑ Enable MEMANTO integration │
│ │
│ MEMANTO Instance URL │
│ ┌──────────────────────────────────────────────────┐ │
│ │ https://abc123.context.wpagentic.dev │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Moorcheh API Key │
│ ┌──────────────────────────────────────────────────┐ │
│ │ •••••••••••••••••••••••• │ │
│ └──────────────────────────────────────────────────┘ │
Get a free API key at moorcheh.ai (10K vectors/month) │
│ │
│ Connection Status: 🟢 Connected │
│ Last checked: 2 minutes ago │
│ │
│ [Test Connection] │
│ │
├─────────────────────────────────────────────────────────────────┤
MEMANTO is an optional add-on that provides persistent │
│ memory for your AI writing assistant. Your AI will remember │
│ context across sessions and posts. The plugin works │
│ perfectly without MEMANTO. │
│ │
│ Get MEMANTO at: wpagentic.dev/memanto │
└─────────────────────────────────────────────────────────────────┘
```
---
## Summary
| Aspect | Decision |
|---|---|
| **Scope** | Optional enhancement, not a dependency |
| **New files** | `class-memanto-client.php`, `class-memanto-context-enhancer.php` |
| **Modified files** | `class-context-builder.php`, `class-context-service.php`, `class-gutenberg-sidebar.php`, `class-settings-v2.php`, `class-autoloader.php` |
| **Frontend changes** | Minimal: status indicator only |
| **Fallback behavior** | Full graceful degradation to MySQL-only context |
| **Implementation time** | 5 weeks (1 week per phase) |
| **Testing priority** | Phase 2 (write-through) and Phase 3 (recall) are critical paths |
---
**Document Date:** June 7, 2026
**Status:** Draft — Ready for Phase 1 implementation

File diff suppressed because it is too large Load Diff

View File

@@ -202,7 +202,7 @@ class WP_Agentic_Writer_Conversation_Manager {
$session = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status = 'active' ORDER BY updated_at DESC LIMIT 1",
"SELECT * FROM {$this->table_name} WHERE post_id = %d AND status != 'archived' ORDER BY updated_at DESC LIMIT 1",
$post_id
),
ARRAY_A

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,636 @@
<?php
/**
* MEMANTO Client
*
* PHP client for MEMANTO API v2 — external semantic memory service.
* Provides persistent, cross-session memory for the WP Agentic Writer plugin.
*
* @package WP_Agentic_Writer
* @since 0.3.0
*/
if (!defined("ABSPATH")) {
exit();
}
/**
* Class WP_Agentic_Writer_Memanto_Client
*
* Communicates with a MEMANTO instance (powered by Moorcheh SDK).
* All calls are wrapped in error handling — failures never disrupt the plugin.
*/
class WP_Agentic_Writer_Memanto_Client
{
/**
* Singleton instance.
*
* @var WP_Agentic_Writer_Memanto_Client
*/
private static $instance = null;
/**
* MEMANTO base URL (e.g. https://abc123.context.wpagentic.dev).
*
* @var string
*/
private $base_url = "";
/**
* Cached health status.
*
* @var array|null { healthy: bool, checked_at: int } or null if not checked yet.
*/
private $health_cache = null;
/**
* Get singleton instance.
*
* @return WP_Agentic_Writer_Memanto_Client
*/
public static function get_instance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct()
{
$settings = get_option("wp_agentic_writer_settings", []);
$this->base_url = untrailingslashit($settings["memanto_url"] ?? "");
}
// =========================================================================
// Configuration & Health
// =========================================================================
/**
* Whether MEMANTO is configured (URL + Moorcheh key set).
*
* @return bool
*/
public function is_configured()
{
return !empty($this->base_url) && !empty($this->get_moorcheh_key());
}
/**
* Whether MEMANTO is enabled in settings.
*
* @return bool
*/
public function is_enabled()
{
$settings = get_option("wp_agentic_writer_settings", []);
return !empty($settings["memanto_enabled"]);
}
/**
* Whether MEMANTO is enabled, configured, and reachable.
*
* @return bool
*/
public function is_active()
{
return $this->is_enabled() &&
$this->is_configured() &&
$this->is_healthy();
}
/**
* Check MEMANTO health endpoint. Result cached for 5 minutes.
*
* @return bool True if healthy.
*/
public function is_healthy()
{
if (!$this->is_configured()) {
return false;
}
// Use cached result if fresh (5 minutes).
if (null !== $this->health_cache) {
if (time() - $this->health_cache["checked_at"] < 300) {
return $this->health_cache["healthy"];
}
}
// Also check transient for cross-request caching.
$cached = get_transient("wpaw_memanto_health");
if (
false !== $cached &&
isset($cached["checked_at"]) &&
time() - $cached["checked_at"] < 300
) {
$this->health_cache = $cached;
return $cached["healthy"];
}
$response = $this->get("/health");
if (is_wp_error($response)) {
$this->health_cache = ["healthy" => false, "checked_at" => time()];
set_transient("wpaw_memanto_health", $this->health_cache, 300);
return false;
}
$healthy =
!empty($response["status"]) && "healthy" === $response["status"];
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
set_transient("wpaw_memanto_health", $this->health_cache, 300);
return $healthy;
}
/**
* Force-refresh the health check (used by Test Connection button).
*
* @return array { healthy: bool, details: array|null }
*/
public function check_health_fresh()
{
if (!$this->is_configured()) {
return ["healthy" => false, "details" => null];
}
delete_transient("wpaw_memanto_health");
$this->health_cache = null;
$response = $this->get("/health");
if (is_wp_error($response)) {
return [
"healthy" => false,
"details" => ["error" => $response->get_error_message()],
];
}
$healthy =
!empty($response["status"]) && "healthy" === $response["status"];
$this->health_cache = ["healthy" => $healthy, "checked_at" => time()];
set_transient("wpaw_memanto_health", $this->health_cache, 300);
return ["healthy" => $healthy, "details" => $response];
}
// =========================================================================
// Agent Management
// =========================================================================
/**
* Ensure an agent exists. Creates if not found.
*
* @param string $agent_id Agent identifier (e.g. "wp-user-1" or "wp-post-42").
* @return bool True on success.
*/
public function ensure_agent($agent_id)
{
if (!$this->is_configured()) {
return false;
}
// Check if agent already exists.
$agent = $this->get("/api/v2/agents/{$agent_id}");
if (!is_wp_error($agent) && !empty($agent["agent_id"])) {
return true;
}
// Create agent.
$result = $this->post("/api/v2/agents", [
"agent_id" => $agent_id,
"pattern" => "support",
"description" => "WP Agentic Writer agent",
]);
return !is_wp_error($result);
}
// =========================================================================
// Session Management
// =========================================================================
/**
* Activate a session for an agent. Returns cached token if still valid.
*
* @param string $agent_id Agent identifier.
* @return string|false Session token or false on failure.
*/
public function activate_session($agent_id)
{
if (!$this->is_configured()) {
return false;
}
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
$cached_token = get_transient($transient_key);
if (!empty($cached_token)) {
return $cached_token;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/activate",
[],
$agent_id,
);
if (is_wp_error($response) || empty($response["session_token"])) {
wpaw_debug_log("MEMANTO activate_session failed", [
"agent_id" => $agent_id,
]);
return false;
}
$token = $response["session_token"];
$expires_at = strtotime($response["expires_at"] ?? "+6 hours");
$ttl = max(60, $expires_at - time() - 300); // Expire 5 min before actual.
set_transient($transient_key, $token, $ttl);
return $token;
}
/**
* Deactivate a session for an agent.
*
* Clears the cached token and sends the deactivate request
* without injecting a session token (avoids re-activation loop).
*
* @param string $agent_id Agent identifier.
* @return bool True on success.
*/
public function deactivate_session($agent_id)
{
$transient_key = "wpaw_memanto_token_" . md5($agent_id);
delete_transient($transient_key);
if (!$this->is_configured()) {
return false;
}
$url = $this->base_url . "/api/v2/agents/{$agent_id}/deactivate";
$headers = $this->get_headers();
$response = wp_remote_post($url, [
"headers" => $headers,
"body" => wp_json_encode([]),
"timeout" => 10,
]);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO deactivate_session failed", [
"agent_id" => $agent_id,
]);
return false;
}
return true;
}
// =========================================================================
// Memory Operations
// =========================================================================
/**
* Store a memory.
*
* @param string $agent_id Agent identifier.
* @param string $content Memory content (max 10000 chars).
* @param string $type Memory type (fact, preference, goal, decision, artifact, learning, event, instruction, relationship, context, observation, commitment, error).
* @param array $tags Optional tags.
* @param string $title Optional title (max 100 chars).
* @return bool True on success.
*/
public function remember(
$agent_id,
$content,
$type = "context",
$tags = [],
$title = "",
) {
if (!$this->is_active()) {
return false;
}
$body = [
"content" => mb_substr($content, 0, 10000),
"type" => $type,
"tags" => $tags,
"source" => "wp-agentic-writer",
];
if (!empty($title)) {
$body["title"] = mb_substr($title, 0, 100);
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/remember",
$body,
$agent_id,
);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO remember failed", [
"agent_id" => $agent_id,
"type" => $type,
"error" => $response->get_error_message(),
]);
return false;
}
return true;
}
/**
* Store multiple memories in batch (max 100).
*
* @param string $agent_id Agent identifier.
* @param array $memories Array of memory items. Each item: { content, type, tags, title }.
* @return bool True on success.
*/
public function batch_remember($agent_id, $memories)
{
if (!$this->is_active() || empty($memories)) {
return false;
}
$batch = [];
foreach (array_slice($memories, 0, 100) as $item) {
$entry = [
"content" => mb_substr($item["content"] ?? "", 0, 10000),
"type" => $item["type"] ?? "context",
"source" => "wp-agentic-writer",
];
if (!empty($item["title"])) {
$entry["title"] = mb_substr($item["title"], 0, 100);
}
if (!empty($item["tags"])) {
$entry["tags"] = $item["tags"];
}
$batch[] = $entry;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/batch-remember",
["memories" => $batch],
$agent_id,
);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO batch_remember failed", [
"agent_id" => $agent_id,
"count" => count($batch),
"error" => $response->get_error_message(),
]);
return false;
}
return true;
}
/**
* Semantic search / recall memories.
*
* @param string $agent_id Agent identifier.
* @param string $query Search query.
* @param array $type_filter Optional memory type filter.
* @param int $limit Max results (default 10).
* @param float $min_similarity Minimum similarity score 0-1.
* @return array Recalled memories (empty on failure).
*/
public function recall(
$agent_id,
$query,
$type_filter = [],
$limit = 10,
$min_similarity = 0.3,
) {
if (!$this->is_active()) {
return [];
}
$body = [
"query" => $query,
"limit" => $limit,
"min_similarity" => $min_similarity,
];
if (!empty($type_filter)) {
$body["type"] = $type_filter;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/recall",
$body,
$agent_id,
);
if (is_wp_error($response)) {
wpaw_debug_log("MEMANTO recall failed", [
"agent_id" => $agent_id,
"query" => substr($query, 0, 100),
"error" => $response->get_error_message(),
]);
return [];
}
return is_array($response) ? $response : [];
}
/**
* Recall most recent memories.
*
* @param string $agent_id Agent identifier.
* @param int $limit Max results.
* @param array $type_filter Optional memory type filter.
* @return array Recent memories (empty on failure).
*/
public function recall_recent($agent_id, $limit = 10, $type_filter = [])
{
if (!$this->is_active()) {
return [];
}
$body = ["limit" => $limit];
if (!empty($type_filter)) {
$body["type"] = $type_filter;
}
$response = $this->post(
"/api/v2/agents/{$agent_id}/recall/recent",
$body,
$agent_id,
);
if (is_wp_error($response)) {
return [];
}
return is_array($response) ? $response : [];
}
// =========================================================================
// Agent ID Builders
// =========================================================================
/**
* Build user-level agent ID.
*
* @param int $user_id WordPress user ID.
* @return string Agent ID like "wp-user-1".
*/
public function get_user_agent_id($user_id)
{
return "wp-user-" . absint($user_id);
}
/**
* Build post-level agent ID.
*
* @param int $post_id WordPress post ID.
* @return string Agent ID like "wp-post-42".
*/
public function get_post_agent_id($post_id)
{
return "wp-post-" . absint($post_id);
}
// =========================================================================
// HTTP Helpers (private)
// =========================================================================
/**
* Get the Moorcheh API key from settings.
*
* @return string
*/
private function get_moorcheh_key()
{
$settings = get_option("wp_agentic_writer_settings", []);
return $settings["memanto_moorcheh_key"] ?? "";
}
/**
* GET request to MEMANTO API.
*
* @param string $path API path (e.g. "/health", "/api/v2/agents/{id}").
* @return array|WP_Error Decoded response or error.
*/
private function get($path)
{
$url = $this->base_url . $path;
$response = wp_remote_get($url, [
"headers" => $this->get_headers(),
"timeout" => 10,
]);
return $this->parse_response($response);
}
/**
* POST request to MEMANTO API.
*
* @param string $path API path.
* @param array $body Request body.
* @param string $agent_id Optional agent ID for session token injection.
* @return array|WP_Error Decoded response or error.
*/
private function post($path, $body = [], $agent_id = "")
{
$url = $this->base_url . $path;
$headers = $this->get_headers();
// Inject session token if we have an agent_id.
if (!empty($agent_id)) {
$token = $this->activate_session($agent_id);
if ($token) {
$headers["X-Session-Token"] = $token;
}
}
$response = wp_remote_post($url, [
"headers" => $headers,
"body" => wp_json_encode($body),
"timeout" => 10,
]);
// Handle expired token (401) — re-activate and retry once.
if (!is_wp_error($response)) {
$code = wp_remote_retrieve_response_code($response);
if (401 === $code && !empty($agent_id)) {
// Clear cached token and retry.
delete_transient("wpaw_memanto_token_" . md5($agent_id));
$token = $this->activate_session($agent_id);
if ($token) {
$headers["X-Session-Token"] = $token;
$response = wp_remote_post($url, [
"headers" => $headers,
"body" => wp_json_encode($body),
"timeout" => 10,
]);
}
}
}
return $this->parse_response($response);
}
/**
* Build common headers for MEMANTO API requests.
*
* @return array Headers.
*/
private function get_headers()
{
return [
"Content-Type" => "application/json",
"Accept" => "application/json",
"X-API-Key" => $this->get_moorcheh_key(),
];
}
/**
* Parse HTTP response from MEMANTO API.
*
* @param array|WP_Error $response wp_remote response.
* @return array|WP_Error Decoded body or error.
*/
private function parse_response($response)
{
if (is_wp_error($response)) {
return new WP_Error(
"memanto_connection_error",
$response->get_error_message(),
);
}
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
if (401 === $code) {
// Let caller handle re-auth.
return new WP_Error(
"memanto_unauthorized",
"Session token expired",
);
}
if ($code >= 400) {
wpaw_debug_log("MEMANTO API error ({$code})", $body);
return new WP_Error(
"memanto_api_error",
sprintf(
"MEMANTO API error (%d): %s",
$code,
substr($body, 0, 200),
),
);
}
$decoded = json_decode($body, true);
return is_array($decoded) ? $decoded : [];
}
}

View File

@@ -0,0 +1,754 @@
<?php
/**
* MEMANTO Context Enhancer
*
* Orchestrates when and what to remember/recall from MEMANTO.
* Hooks into the existing Context Service to provide memory
* enrichment without replacing any existing behavior.
*
* @package WP_Agentic_Writer
* @since 0.3.0
*/
if (!defined("ABSPATH")) {
exit();
}
/**
* Class WP_Agentic_Writer_Memanto_Context_Enhancer
*
* Every public method checks is_active() first and returns gracefully
* if MEMANTO is not available. The plugin never breaks.
*/
class WP_Agentic_Writer_Memanto_Context_Enhancer
{
/**
* Singleton instance.
*
* @var WP_Agentic_Writer_Memanto_Context_Enhancer
*/
private static $instance = null;
/**
* MEMANTO client reference.
*
* @var WP_Agentic_Writer_Memanto_Client
*/
private $client;
/**
* Get singleton instance.
*
* @return WP_Agentic_Writer_Memanto_Context_Enhancer
*/
public static function get_instance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*
* Registers all WordPress action/filter hooks so that the sidebar
* handlers only need a single do_action() call at each trigger point.
*/
private function __construct()
{
$this->client = WP_Agentic_Writer_Memanto_Client::get_instance();
// Session lifecycle.
add_action(
"wpaw_memanto_session_start",
[$this, "on_session_start"],
10,
3,
);
add_action(
"wpaw_memanto_session_end",
[$this, "on_session_end"],
10,
2,
);
// Write-through: remember on meaningful events.
add_action(
"wpaw_memanto_user_message",
[$this, "on_user_message"],
10,
3,
);
add_action(
"wpaw_memanto_plan_generated",
[$this, "on_plan_generated"],
10,
2,
);
add_action(
"wpaw_memanto_plan_approved",
[$this, "on_plan_approved"],
10,
2,
);
add_action(
"wpaw_memanto_plan_rejected",
[$this, "on_plan_rejected"],
10,
2,
);
add_action(
"wpaw_memanto_section_written",
[$this, "on_section_written"],
10,
3,
);
add_action(
"wpaw_memanto_block_refined",
[$this, "on_block_refined"],
10,
3,
);
add_action(
"wpaw_memanto_config_saved",
[$this, "on_config_saved"],
10,
2,
);
}
// =========================================================================
// Session Lifecycle
// =========================================================================
/**
* Called when a conversation session starts.
* Ensures agents exist and recalls previous session state.
*
* @param string $session_id Session ID.
* @param int $post_id Post ID.
* @param int $user_id WordPress user ID.
*/
public function on_session_start($session_id, $post_id, $user_id)
{
if (!$this->client->is_active()) {
return;
}
// Ensure user agent exists.
if ($user_id > 0) {
$this->client->ensure_agent(
$this->client->get_user_agent_id($user_id),
);
}
// Ensure post agent exists.
if ($post_id > 0) {
$this->client->ensure_agent(
$this->client->get_post_agent_id($post_id),
);
}
}
/**
* Called when a conversation session ends.
* Stores a session summary memory and deactivates the session.
*
* @param string $session_id Session ID.
* @param int $post_id Post ID.
*/
public function on_session_end($session_id, $post_id)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$post_agent = $this->client->get_post_agent_id($post_id);
// Store a session summary.
$this->client->remember(
$post_agent,
"Session ended: " . $session_id,
"context",
["session:" . $session_id, "post:" . $post_id],
"Session end",
);
// Deactivate MEMANTO session to trigger summary generation.
$this->client->deactivate_session($post_agent);
}
// =========================================================================
// Write-Through: Remember on Meaningful Events
// =========================================================================
/**
* Store a memory when user sends a chat message.
*
* @param string $session_id Session ID.
* @param string $content User message content.
* @param int $post_id Post ID.
*/
public function on_user_message($session_id, $content, $post_id)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"User instruction: " . wp_strip_all_tags($content),
"instruction",
["post:" . $post_id, "session:" . $session_id],
);
}
/**
* Store a memory when a plan is generated.
*
* @param int $post_id Post ID.
* @param array $plan Plan data.
*/
public function on_plan_generated($post_id, $plan)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$title = $plan["title"] ?? "Untitled";
$sections =
isset($plan["sections"]) && is_array($plan["sections"])
? count($plan["sections"])
: 0;
$this->client->remember(
$this->client->get_post_agent_id($post_id),
sprintf('Plan generated: "%s" with %d sections', $title, $sections),
"artifact",
["post:" . $post_id, "type:plan"],
"Plan: " . $title,
);
}
/**
* Store a memory when user approves a plan.
*
* @param int $post_id Post ID.
* @param array $plan Plan data.
*/
public function on_plan_approved($post_id, $plan)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$title = $plan["title"] ?? "Untitled";
$this->client->remember(
$this->client->get_post_agent_id($post_id),
sprintf('User approved plan: "%s"', $title),
"decision",
["post:" . $post_id, "type:plan"],
"Plan approved",
);
}
/**
* Store a memory when user rejects or requests plan changes.
*
* @param int $post_id Post ID.
* @param string $reason Rejection/revision reason.
*/
public function on_plan_rejected($post_id, $reason)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"User requested plan revision: " . wp_strip_all_tags($reason),
"error",
["post:" . $post_id, "type:plan"],
"Plan revision",
);
}
/**
* Store a memory when a section is written.
*
* @param int $post_id Post ID.
* @param string $section_id Section identifier.
* @param string $summary Brief section summary.
*/
public function on_section_written($post_id, $section_id, $summary)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"Section written (" .
$section_id .
"): " .
wp_strip_all_tags($summary),
"artifact",
["post:" . $post_id, "section:" . $section_id],
"Section: " . $section_id,
);
}
/**
* Store a memory when a block is refined.
*
* @param int $post_id Post ID.
* @param string $block_id Block identifier.
* @param string $instruction Refinement instruction.
*/
public function on_block_refined($post_id, $block_id, $instruction)
{
if (!$this->client->is_active() || $post_id <= 0) {
return;
}
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"Block refined (" .
$block_id .
"): " .
wp_strip_all_tags($instruction),
"instruction",
["post:" . $post_id, "block:" . $block_id],
);
}
/**
* Store a memory when post config is saved.
*
* @param int $post_id Post ID.
* @param array $config Post config data.
*/
public function on_config_saved($post_id, $config)
{
if (!$this->client->is_active()) {
return;
}
$config_summary = sprintf(
"tone=%s, audience=%s, length=%s, language=%s",
$config["tone"] ?? "default",
$config["audience"] ?? "general",
$config["article_length"] ?? "medium",
$config["language"] ?? "auto",
);
// Store to post agent.
if ($post_id > 0) {
$this->client->remember(
$this->client->get_post_agent_id($post_id),
"Article config: " . $config_summary,
"preference",
["post:" . $post_id],
"Post config",
);
}
// Store to user agent (cross-post preferences).
$user_id = get_current_user_id();
if ($user_id > 0) {
$this->client->remember(
$this->client->get_user_agent_id($user_id),
"User preference: " . $config_summary,
"preference",
["user:" . $user_id],
"Writing preferences",
);
}
}
// =========================================================================
// Recall: Retrieve Memories for Context Enrichment
// =========================================================================
/**
* Recall relevant memories to enrich AI prompt context.
*
* @param int $post_id Post ID.
* @param int $user_id WordPress user ID.
* @param string $current_message User's current message (for semantic search).
* @return array Recalled memory items, each with 'type', 'content', 'title'.
*/
public function recall_for_context(
$post_id,
$user_id,
$current_message = "",
) {
if (!$this->client->is_active()) {
return [];
}
$memories = [];
$seen = [];
// 1. Recent post memories.
if ($post_id > 0) {
$post_agent = $this->client->get_post_agent_id($post_id);
$recent = $this->client->recall_recent($post_agent, 10);
foreach ($this->normalize_memories($recent) as $item) {
$hash = md5($item["content"]);
if (!isset($seen[$hash])) {
$seen[$hash] = true;
$memories[] = $item;
}
}
// 2. Semantic recall based on current message.
if (!empty($current_message)) {
$semantic = $this->client->recall(
$post_agent,
$current_message,
[],
5,
);
foreach ($this->normalize_memories($semantic) as $item) {
$hash = md5($item["content"]);
if (!isset($seen[$hash])) {
$seen[$hash] = true;
$memories[] = $item;
}
}
}
}
// 3. User preferences (cross-post).
if ($user_id > 0) {
$user_prefs = $this->client->recall(
$this->client->get_user_agent_id($user_id),
"writing preferences tone audience language",
["preference"],
5,
);
foreach ($this->normalize_memories($user_prefs) as $item) {
$hash = md5($item["content"]);
if (!isset($seen[$hash])) {
$seen[$hash] = true;
$memories[] = $item;
}
}
}
return $memories;
}
/**
* Restore session state from MEMANTO when reopening a post.
*
* Returns a structured restore payload that the frontend can use to:
* - Display a "Restored from memory" badge
* - Build a restored-session system message for the AI
* - Pre-fill config from prior preferences
*
* @since 0.4.0
* @param int $post_id Post ID being reopened.
* @param int $user_id Current user ID.
* @return array { restored: bool, memories: array, preferences: array, summary: string }
*/
public function restore_session($post_id, $user_id)
{
$empty = [
"restored" => false,
"memories" => [],
"preferences" => [],
"summary" => "",
];
if (!$this->client->is_active() || $post_id <= 0) {
return $empty;
}
// Recall recent post memories (no current message — restore is about history).
$post_agent = $this->client->get_post_agent_id($post_id);
$recent = $this->client->recall_recent($post_agent, 15);
$memories = $this->normalize_memories($recent);
// Recall user preferences (for cross-post config carry-over).
$preferences = [];
if ($user_id > 0) {
$user_prefs = $this->client->recall(
$this->client->get_user_agent_id($user_id),
"writing preferences tone audience language length",
["preference"],
5,
);
$preferences = $this->normalize_memories($user_prefs);
}
if (empty($memories) && empty($preferences)) {
return $empty;
}
// Build a human-readable summary for the restore badge tooltip.
$summary = $this->build_restore_summary($memories, $preferences);
return [
"restored" => true,
"memories" => $memories,
"preferences" => $preferences,
"summary" => $summary,
];
}
/**
* Build a compact summary string of restored memories.
*
* @param array $memories Restored memories.
* @param array $preferences Restored preferences.
* @return string Summary text.
*/
private function build_restore_summary($memories, $preferences)
{
$parts = [];
if (!empty($memories)) {
$parts[] = sprintf(
/* translators: %d: number of memories */
_n(
"%d memory",
"%d memories",
count($memories),
"wp-agentic-writer",
),
count($memories),
);
}
if (!empty($preferences)) {
$parts[] = sprintf(
/* translators: %d: number of preferences */
_n(
"%d preference",
"%d preferences",
count($preferences),
"wp-agentic-writer",
),
count($preferences),
);
}
return implode(", ", $parts);
}
/**
* Get user's writing preferences recalled from MEMANTO.
*
* Used when creating a new post to pre-fill the post config
* with the user's habitual tone, audience, etc.
*
* @since 0.4.0
* @param int $user_id WordPress user ID.
* @return array { restored: bool, config: array }
*/
public function get_user_preferences_for_new_post($user_id)
{
$empty = ["restored" => false, "config" => []];
if (!$this->client->is_active() || $user_id <= 0) {
return $empty;
}
$user_prefs = $this->client->recall(
$this->client->get_user_agent_id($user_id),
"writing preferences tone audience language length",
["preference"],
3,
);
$prefs = $this->normalize_memories($user_prefs);
if (empty($prefs)) {
return $empty;
}
// Extract config fields from preference content.
// Memory format: "User preference: tone=professional, audience=experts, length=long, language=en"
$config = $this->extract_config_from_preferences($prefs);
if (empty($config)) {
return $empty;
}
return ["restored" => true, "config" => $config];
}
/**
* Parse preference memory contents and extract config fields.
*
* @param array $prefs Normalized preference memories.
* @return array Extracted config: { tone, audience, article_length, language }.
*/
private function extract_config_from_preferences($prefs)
{
$config = [];
// Combine all preference content into one blob for parsing.
$blob = "";
foreach ($prefs as $pref) {
$blob .= " " . ($pref["content"] ?? "");
}
// Match key=value patterns.
if (preg_match('/tone\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "default" !== strtolower($val)) {
$config["tone"] = $val;
}
}
if (preg_match('/audience\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "general" !== strtolower($val)) {
$config["audience"] = $val;
}
}
if (preg_match('/(?:article_)?length\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "medium" !== strtolower($val)) {
$config["article_length"] = $val;
}
}
if (preg_match('/language\s*=\s*([^,\n]+)/i', $blob, $m)) {
$val = trim($m[1]);
if ("" !== $val && "auto" !== strtolower($val)) {
$config["language"] = $val;
}
}
return $config;
}
/**
* Build a "restored session" system message for the AI.
*
* This message summarizes prior post work so the AI can resume
* mid-article without re-asking the user for context.
*
* @since 0.4.0
* @param array $restore_payload Result from restore_session().
* @return string System message content (empty if nothing to restore).
*/
public function build_session_restore_message($restore_payload)
{
if (empty($restore_payload["restored"])) {
return "";
}
$memories = $restore_payload["memories"] ?? [];
$preferences = $restore_payload["preferences"] ?? [];
if (empty($memories) && empty($preferences)) {
return "";
}
$lines = ["SESSION RESTORED FROM MEMORY"];
$lines[] =
"The user has returned to this post. Below is context from prior sessions.";
$lines[] =
"Use this to continue where the conversation left off without re-asking.";
// Summarize prior post work.
if (!empty($memories)) {
$lines[] = "";
$lines[] = "## Prior session activity";
$grouped = [];
foreach ($memories as $memory) {
$type = $memory["type"] ?? "context";
if (!isset($grouped[$type])) {
$grouped[$type] = [];
}
$grouped[$type][] = $memory;
}
// Render in priority order: plan > decision > instruction > artifact > error > context.
$priority = [
"artifact",
"decision",
"instruction",
"error",
"learning",
"context",
"preference",
];
foreach ($priority as $type) {
if (empty($grouped[$type])) {
continue;
}
$type_label = ucfirst($type) . "s";
$lines[] = "### {$type_label}";
foreach ($grouped[$type] as $m) {
$content = trim($m["content"] ?? "");
if ("" === $content) {
continue;
}
$title = !empty($m["title"])
? " [" . trim($m["title"]) . "]"
: "";
$lines[] = "- {$content}{$title}";
}
}
}
// Append user preferences (compact).
if (!empty($preferences)) {
$lines[] = "";
$lines[] = "## User writing preferences";
foreach ($preferences as $pref) {
$content = trim($pref["content"] ?? "");
if ("" !== $content) {
$lines[] = "- {$content}";
}
}
}
return implode("\n", $lines);
}
/**
* Normalize raw MEMANTO response into a uniform array.
*
* @param array $raw Raw response from recall/recall_recent.
* @return array Normalized items: { type, content, title }.
*/
private function normalize_memories($raw)
{
if (!is_array($raw)) {
return [];
}
// Handle different possible response shapes.
$items = $raw;
// If response has a 'memories' or 'results' key, unwrap.
if (isset($raw["memories"]) && is_array($raw["memories"])) {
$items = $raw["memories"];
} elseif (isset($raw["results"]) && is_array($raw["results"])) {
$items = $raw["results"];
}
$normalized = [];
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$normalized[] = [
"type" => $item["type"] ?? "context",
"content" => $item["content"] ?? ($item["text"] ?? ""),
"title" => $item["title"] ?? "",
];
}
return $normalized;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,24 +6,24 @@
* @var array $view_data Prepared view data from class-settings-v2.php
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
if (!defined("ABSPATH")) {
exit();
}
// Extract view data for easier access
extract( $view_data );
extract($view_data);
?>
<div class="wrap wpaw-settings-v2-wrap">
<!-- Agentic IDE Split View Layout -->
<div class="wpaw-ide-container d-flex">
<!-- Left Sidebar: Settings Navigation -->
<div class="wpaw-sidebar-nav flex-shrink-0">
<!-- Header inside Sidebar -->
<div class="wpaw-sidebar-header p-3 mb-2 border-bottom border-dark">
<div class="d-flex align-items-center gap-2">
<img src="<?php echo esc_url( WP_AGENTIC_WRITER_URL . 'assets/img/icon.svg' ); ?>"
alt="WP Agentic Writer"
<img src="<?php echo esc_url(WP_AGENTIC_WRITER_URL . "assets/img/icon.svg"); ?>"
alt="WP Agentic Writer"
style="width: 24px; height: 24px; filter: invert(1)">
<h1 class="h6 mb-0 text-white fw-bold">Agentic Writer</h1>
</div>
@@ -39,19 +39,25 @@ extract( $view_data );
<li class="nav-item" role="presentation">
<button class="nav-link active w-100 text-start d-flex align-items-center gap-2" id="general-tab" data-bs-toggle="pill" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">
<i class="bi bi-sliders"></i>
<?php esc_html_e( 'General', 'wp-agentic-writer' ); ?>
<?php esc_html_e("General", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="models-tab" data-bs-toggle="pill" data-bs-target="#models" type="button" role="tab" aria-controls="models" aria-selected="false">
<i class="bi bi-stars"></i>
<?php esc_html_e( 'AI Models', 'wp-agentic-writer' ); ?>
<?php esc_html_e("AI Models", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="local-backend-tab" data-bs-toggle="pill" data-bs-target="#local-backend" type="button" role="tab" aria-controls="local-backend" aria-selected="false">
<i class="bi bi-house-fill"></i>
<?php esc_html_e( 'Local Backend', 'wp-agentic-writer' ); ?>
<?php esc_html_e("Local Backend", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="memanto-tab" data-bs-toggle="pill" data-bs-target="#memanto" type="button" role="tab" aria-controls="memanto" aria-selected="false">
<i class="bi bi-cpu"></i>
<?php esc_html_e("MEMANTO", "wp-agentic-writer"); ?>
</button>
</li>
@@ -59,13 +65,13 @@ extract( $view_data );
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="cost-log-tab" data-bs-toggle="pill" data-bs-target="#cost-log" type="button" role="tab" aria-controls="cost-log" aria-selected="false">
<i class="bi bi-graph-up"></i>
<?php esc_html_e( 'OpenRouter Cost Log', 'wp-agentic-writer' ); ?>
<?php esc_html_e("OpenRouter Cost Log", "wp-agentic-writer"); ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center gap-2" id="guide-tab" data-bs-toggle="pill" data-bs-target="#guide" type="button" role="tab" aria-controls="guide" aria-selected="false">
<i class="bi bi-book"></i>
<?php esc_html_e( 'Model Guide', 'wp-agentic-writer' ); ?>
<?php esc_html_e("Model Guide", "wp-agentic-writer"); ?>
</button>
</li>
</ul>
@@ -75,7 +81,7 @@ extract( $view_data );
<!-- Right Content Pane: Settings Forms -->
<div class="wpaw-content-pane flex-grow-1 d-flex flex-column h-100">
<form method="post" action="options.php" id="wpaw-settings-form" class="h-100 d-flex flex-column">
<?php settings_fields( 'wp_agentic_writer_settings' ); ?>
<?php settings_fields("wp_agentic_writer_settings"); ?>
<!-- Workflow Pipeline Progress -->
<div class="wpaw-workflow-progress wpaw-workflow-compact mb-4" id="wpaw-workflow-display">
@@ -140,7 +146,7 @@ extract( $view_data );
<h2 class="h4 text-white m-0">General Settings</h2>
<p class="text-secondary small mt-1">Configure global API keys, budget, and content parameters.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-general.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-general.php"; ?>
</div>
<!-- Models Tab -->
@@ -149,7 +155,7 @@ extract( $view_data );
<h2 class="h4 text-white m-0">AI Models</h2>
<p class="text-secondary small mt-1">Select logic engines for different stages of the writing pipeline.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-models.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-models.php"; ?>
</div>
<!-- Local Backend Tab -->
@@ -158,16 +164,25 @@ extract( $view_data );
<h2 class="h4 text-white m-0">Local Backend</h2>
<p class="text-secondary small mt-1">Configure connections to local LM Studio or Ollama instances.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-local-backend.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-local-backend.php"; ?>
</div>
<!-- Cost Log Tab -->
<!-- MEMANTO Tab -->
<div class="tab-pane fade" id="memanto" role="tabpanel" aria-labelledby="memanto-tab">
<div class="mb-4 pb-3 border-bottom border-dark">
<h2 class="h4 text-white m-0">MEMANTO Context Keeper</h2>
<p class="text-secondary small mt-1">Optional persistent memory for your AI writing assistant. The plugin works perfectly without it.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-memanto.php"; ?>
</div>
<!-- Cost Log Tab -->
<div class="tab-pane fade" id="cost-log" role="tabpanel" aria-labelledby="cost-log-tab">
<div class="mb-4 pb-3 border-bottom border-dark">
<h2 class="h4 text-white m-0">OpenRouter Cost Analytics</h2>
<p class="text-secondary small mt-1">Track API token usage and expenses across all generations.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-cost-log.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-cost-log.php"; ?>
</div>
<!-- Guide Tab -->
@@ -176,7 +191,7 @@ extract( $view_data );
<h2 class="h4 text-white m-0">Provider Documentation</h2>
<p class="text-secondary small mt-1">Reference materials for selecting the right model constraints.</p>
</div>
<?php include WP_AGENTIC_WRITER_DIR . 'views/settings/tab-guide.php'; ?>
<?php include WP_AGENTIC_WRITER_DIR . "views/settings/tab-guide.php"; ?>
</div>
</div>
</div>
@@ -185,18 +200,28 @@ extract( $view_data );
<div class="wpaw-save-bar p-3 border-top border-dark d-flex justify-content-between align-items-center bg-transparent mt-auto sticky-bottom">
<div class="text-secondary small d-flex align-items-center gap-2">
<span class="dashicons dashicons-plugin text-primary"></span>
<?php printf( esc_html__( 'v%s', 'wp-agentic-writer' ), esc_html( WP_AGENTIC_WRITER_VERSION ) ); ?>
<?php printf(
esc_html__("v%s", "wp-agentic-writer"),
esc_html(WP_AGENTIC_WRITER_VERSION),
); ?>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="wpaw-reset-settings">
<?php esc_html_e( 'Reset Defaults', 'wp-agentic-writer' ); ?>
<?php esc_html_e("Reset Defaults", "wp-agentic-writer"); ?>
</button>
<button type="submit" class="btn btn-sm btn-primary px-4 fw-semibold" id="wpaw-save-settings">
<?php
$is_mac = isset( $_SERVER['HTTP_USER_AGENT'] ) && strpos( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ), 'Mac OS' ) !== false;
$cmd_key = $is_mac ? '⌘' : 'Ctrl';
?>
<?php esc_html_e( 'Save Settings', 'wp-agentic-writer' ); ?> <kbd class="ms-1 bg-dark text-white border-0 py-0"><?php echo esc_html( $cmd_key ); ?>+S</kbd>
$is_mac =
isset($_SERVER["HTTP_USER_AGENT"]) &&
strpos(wp_unslash($_SERVER["HTTP_USER_AGENT"]), "Mac OS") !== false;
$cmd_key = $is_mac ? "⌘" : "Ctrl";
?>
<?php esc_html_e(
"Save Settings",
"wp-agentic-writer",
); ?> <kbd class="ms-1 bg-dark text-white border-0 py-0"><?php echo esc_html(
$cmd_key,
); ?>+S</kbd>
</button>
</div>
</div>
@@ -210,7 +235,10 @@ extract( $view_data );
<div id="wpaw-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<span class="me-2">✨</span>
<strong class="me-auto"><?php esc_html_e( 'WP Agentic Writer', 'wp-agentic-writer' ); ?></strong>
<strong class="me-auto"><?php esc_html_e(
"WP Agentic Writer",
"wp-agentic-writer",
); ?></strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="wpaw-toast-message"></div>

View File

@@ -66,7 +66,15 @@ if ( $ai_client_available ) {
<span class="text-danger ms-2">*</span>
</label>
<div class="form-text">
<?php printf( wp_kses_post( __( 'Get your API key from <a href="%s" target="_blank" class="text-decoration-none">OpenRouter <i class="bi bi-box-arrow-up-right"></i></a>', 'wp-agentic-writer' ) ), 'https://openrouter.ai/keys' ); ?>
<?php printf(
wp_kses_post(
__(
'Get your API key from <a href="%s" target="_blank" class="text-decoration-none">OpenRouter <i class="bi bi-box-arrow-up-right"></i></a>',
"wp-agentic-writer",
),
),
"https://openrouter.ai/keys",
); ?>
</div>
<div class="input-group">
<span class="input-group-text"><span class="dashicons dashicons-admin-network"></span></span>

View File

@@ -0,0 +1,195 @@
<?php
/**
* Settings Tab: MEMANTO Context Keeper
*
* @package WP_Agentic_Writer
* @since 0.3.0
* @var array $view_data Prepared view data (extracted in layout.php)
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Variables from $view_data (extracted in layout.php).
$memanto_enabled = $memanto_enabled ?? false;
$memanto_url = $memanto_url ?? '';
$memanto_moorcheh_key = $memanto_moorcheh_key ?? '';
?>
<div class="row g-4">
<!-- Enable MEMANTO -->
<div class="col-12">
<div class="wpaw-card">
<div class="wpaw-card-body">
<div class="form-check form-switch mb-0">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="memanto_enabled"
name="wp_agentic_writer_settings[memanto_enabled]"
value="1"
<?php checked( $memanto_enabled ); ?>
>
<label class="form-check-label text-white fw-medium" for="memanto_enabled">
<?php esc_html_e( 'Enable MEMANTO Integration', 'wp-agentic-writer' ); ?>
</label>
</div>
<p class="text-secondary small mt-2 mb-0">
When enabled, the AI writing assistant will store and recall memories across sessions using MEMANTO.
The plugin works perfectly without MEMANTO — this is an optional enhancement.
</p>
</div>
</div>
</div>
<!-- MEMANTO Instance URL -->
<div class="col-md-6">
<div class="wpaw-card h-100">
<div class="wpaw-card-body">
<label for="memanto_url" class="form-label text-white fw-medium">
<i class="bi bi-link-45deg me-1"></i>
<?php esc_html_e( 'MEMANTO Instance URL', 'wp-agentic-writer' ); ?>
</label>
<input
type="url"
class="form-control"
id="memanto_url"
name="wp_agentic_writer_settings[memanto_url]"
value="<?php echo esc_attr( $memanto_url ); ?>"
placeholder="https://your-instance.context.wpagentic.dev"
autocomplete="off"
>
<div class="form-text text-secondary">
The URL of your MEMANTO instance. Provided when you subscribe to MEMANTO Context Keeper.
</div>
</div>
</div>
</div>
<!-- Moorcheh API Key -->
<div class="col-md-6">
<div class="wpaw-card h-100">
<div class="wpaw-card-body">
<label for="memanto_moorcheh_key" class="form-label text-white fw-medium">
<i class="bi bi-key me-1"></i>
<?php esc_html_e( 'Moorcheh API Key', 'wp-agentic-writer' ); ?>
</label>
<input
type="password"
class="form-control"
id="memanto_moorcheh_key"
name="wp_agentic_writer_settings[memanto_moorcheh_key]"
value="<?php echo esc_attr( $memanto_moorcheh_key ); ?>"
placeholder="Enter your Moorcheh API key"
autocomplete="new-password"
>
<div class="form-text text-secondary">
Get a free API key at <a href="https://moorcheh.ai" target="_blank" class="text-info">moorcheh.ai</a> (10K vectors/month included).
Billed to your own Moorcheh account.
</div>
</div>
</div>
</div>
<!-- Connection Status & Test -->
<div class="col-12">
<div class="wpaw-card">
<div class="wpaw-card-body">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div class="d-flex align-items-center gap-3">
<span id="memanto-status-indicator" class="badge rounded-pill bg-secondary">
<?php if ( ! empty( $memanto_url ) && ! empty( $memanto_moorcheh_key ) ) : ?>
<span class="spinner-border spinner-border-sm me-1" role="status"></span> Checking...
<?php else : ?>
Not configured
<?php endif; ?>
</span>
<span id="memanto-status-detail" class="text-secondary small"></span>
</div>
<button
type="button"
class="btn btn-sm btn-outline-info"
id="wpaw-test-memanto"
<?php echo ( empty( $memanto_url ) || empty( $memanto_moorcheh_key ) ) ? 'disabled' : ''; ?>
>
<i class="bi bi-wifi me-1"></i>
<?php esc_html_e( 'Test Connection', 'wp-agentic-writer' ); ?>
</button>
</div>
</div>
</div>
</div>
<!-- Info Box -->
<div class="col-12">
<div class="wpaw-card border-info" style="border-left: 3px solid var(--bs-info);">
<div class="wpaw-card-body">
<div class="d-flex gap-3">
<span class="fs-4">🧠</span>
<div>
<h6 class="text-white mb-2">What MEMANTO does</h6>
<ul class="text-secondary small mb-2 ps-3">
<li>Your AI assistant <strong class="text-white">remembers context</strong> across sessions and browser refreshes</li>
<li>Writing preferences <strong class="text-white">carry over</strong> between posts</li>
<li>Eliminates <strong class="text-white">context loss</strong> when switching between Chat, Planning, and Writing modes</li>
<li>Reduces AI token usage by up to <strong class="text-white">63%</strong> through smart memory recall</li>
</ul>
<p class="text-secondary small mb-0">
Don't have MEMANTO? <a href="https://wpagentic.dev/memanto" target="_blank" class="text-info">Get MEMANTO Context Keeper</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
// Test MEMANTO connection.
$('#wpaw-test-memanto').on('click', function() {
var btn = $(this);
var statusEl = $('#memanto-status-indicator');
var detailEl = $('#memanto-status-detail');
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span> Testing...');
statusEl.removeClass('bg-success bg-danger bg-secondary bg-warning').addClass('bg-warning').html('Testing...');
detailEl.text('');
$.post(wpawSettingsV2.ajaxUrl, {
action: 'wpaw_test_memanto',
nonce: wpawSettingsV2.nonce,
url: $('#memanto_url').val(),
key: $('#memanto_moorcheh_key').val()
}, function(response) {
btn.prop('disabled', false).html('<i class="bi bi-wifi me-1"></i> Test Connection');
if (response.success && response.data.healthy) {
statusEl.removeClass('bg-warning bg-danger bg-secondary').addClass('bg-success').html('🟢 Connected');
var detail = response.data.details || {};
var info = detail.service ? detail.service + ' v' + (detail.version || '?') : 'Healthy';
if (detail.moorcheh_connected) {
info += ' · Moorcheh connected';
}
detailEl.text(info);
} else {
statusEl.removeClass('bg-warning bg-success bg-secondary').addClass('bg-danger').html('🔴 Error');
detailEl.text(response.data?.message || 'Connection failed');
}
}).fail(function() {
btn.prop('disabled', false).html('<i class="bi bi-wifi me-1"></i> Test Connection');
statusEl.removeClass('bg-warning bg-success bg-secondary').addClass('bg-danger').html('🔴 Error');
detailEl.text('Request failed');
});
});
// Auto-test on load if both fields are filled.
if ($('#memanto_url').val() && $('#memanto_moorcheh_key').val()) {
$('#wpaw-test-memanto').trigger('click');
}
});
</script>