diff --git a/.distpackage b/.distpackage index 6d41aa3..c172ab6 100644 --- a/.distpackage +++ b/.distpackage @@ -5,5 +5,3 @@ uninstall.php build-extensions/ languages/ readme.txt -README.md -docs/ diff --git a/.eslintrc.js b/.eslintrc.js index 741348b..51af2dd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,9 +25,21 @@ module.exports = { // Allow nested ternary in specific cases (model loader status messages) 'no-nested-ternary': 'warn', - // JSDoc: Make missing descriptions a warning instead of error + // We intentionally use HStack/VStack/etc. for now. Track v0.12 migration + // to stable APIs separately when @wordpress/components publishes them. + '@wordpress/no-unsafe-wp-apis': 'warn', + + // Unused vars are noise, not bugs. Track + clean up in a dedicated pass. + 'no-unused-vars': 'warn', + + // JSDoc: missing descriptions/types are downgraded to warnings — the + // recommended preset is too aggressive for our hybrid JS+JSX codebase. 'jsdoc/require-returns-description': 'warn', 'jsdoc/require-param-description': 'warn', + 'jsdoc/require-param-type': 'warn', + 'jsdoc/require-returns-type': 'warn', + 'jsdoc/require-param': 'warn', + 'jsdoc/require-returns': 'warn', // JSX type is valid in WordPress component returns 'jsdoc/no-undefined-types': [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66058d8..172bffb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,4 +63,9 @@ jobs: run: composer install --no-interaction --prefer-dist - name: Run ${{ matrix.check_name }} - run: ${{ matrix.command }} \ No newline at end of file + run: ${{ matrix.command }} + + # WordPress.org Plugin Check is run locally against an extracted dist zip + # in a Playground instance — see Section "Plugin Check" in CONTRIBUTING.md. + # Adding it to GHA requires wp-env bootstrap that's worth doing in a focused + # follow-up; tracked separately for v0.12. \ No newline at end of file diff --git a/.wordpress-org/.gitignore b/.wordpress-org/.gitignore new file mode 100644 index 0000000..5f027ec --- /dev/null +++ b/.wordpress-org/.gitignore @@ -0,0 +1,7 @@ +# Scratch capture + AI generation artifacts. Design source (_artboards.html) +# is tracked so the listing assets can be regenerated later. +_preview.png +_banner_raw.png +_icon_raw.png +_banner_nb2.png +_icon_nb2.png diff --git a/.wordpress-org/_artboards.html b/.wordpress-org/_artboards.html new file mode 100644 index 0000000..ea0f52d --- /dev/null +++ b/.wordpress-org/_artboards.html @@ -0,0 +1,214 @@ + + + + +Agentic Admin — WP.org Assets + + + + + + + +
+
icon-256x256.png
+
+
AA
+
+
+
+ +
+
icon-128x128.png
+
+
AA
+
+
+
+ +
+
banner-772x250.png
+ +
+ +
+
banner-1544x500.png
+ +
+ + + diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png new file mode 100644 index 0000000..01df806 Binary files /dev/null and b/.wordpress-org/banner-1544x500.png differ diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png new file mode 100644 index 0000000..3d2deb0 Binary files /dev/null and b/.wordpress-org/banner-772x250.png differ diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png new file mode 100644 index 0000000..af31b7c Binary files /dev/null and b/.wordpress-org/icon-128x128.png differ diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png new file mode 100644 index 0000000..34a55e0 Binary files /dev/null and b/.wordpress-org/icon-256x256.png differ diff --git a/.wordpress-org/screenshot-1.png b/.wordpress-org/screenshot-1.png new file mode 100644 index 0000000..f30236b Binary files /dev/null and b/.wordpress-org/screenshot-1.png differ diff --git a/.wordpress-org/screenshot-2.png b/.wordpress-org/screenshot-2.png new file mode 100644 index 0000000..3bcda7b Binary files /dev/null and b/.wordpress-org/screenshot-2.png differ diff --git a/.wordpress-org/screenshot-3.png b/.wordpress-org/screenshot-3.png new file mode 100644 index 0000000..a8ee148 Binary files /dev/null and b/.wordpress-org/screenshot-3.png differ diff --git a/.wordpress-org/screenshot-4.png b/.wordpress-org/screenshot-4.png new file mode 100644 index 0000000..26325c3 Binary files /dev/null and b/.wordpress-org/screenshot-4.png differ diff --git a/.wordpress-org/screenshot-5.png b/.wordpress-org/screenshot-5.png new file mode 100644 index 0000000..cb786ac Binary files /dev/null and b/.wordpress-org/screenshot-5.png differ diff --git a/.wordpress-org/screenshot-6.png b/.wordpress-org/screenshot-6.png new file mode 100644 index 0000000..168b193 Binary files /dev/null and b/.wordpress-org/screenshot-6.png differ diff --git a/docs/UI-AUDIT-WP-7.0.md b/docs/UI-AUDIT-WP-7.0.md new file mode 100644 index 0000000..883aeb8 --- /dev/null +++ b/docs/UI-AUDIT-WP-7.0.md @@ -0,0 +1,218 @@ +# UI Audit — WordPress 7.0 + +**Issue:** [#220 — Wider UI review to match WordPress 7.0 admin styling](https://github.com/pluginslab/wp-agentic-admin/issues/220) +**Target milestone:** v0.13 — WordPress.org Submission (with v0.14 spillover) +**Date:** 2026-05-23 +**Plugin version at audit:** 0.12.0 + +## Framing — what this audit actually is + +The issue describes "WordPress 7.0 just shipped and introduces refreshed admin UI styling." After diffing the shipped 7.0 files against 6.9.4 locally, **that framing is partially wrong**. The legacy admin shell (`wp-admin/css/common.css`, button classes, color tokens, card primitives) is visually unchanged in 7.0. Custom admin screens will not look "out of date" on 7.0 by default. + +What *did* change and *does* affect this plugin: + +1. **`@wordpress/components` jumped from 30.6.5 (in WP 6.9) to 32.2.0 (in WP 7.0)** with breaking default changes — most notably `__nextHasNoMarginBottom = true` on every form control. Stacked controls in our Settings tab will visually collapse. +2. **`@wordpress/admin-ui` removed from WP Core.** Still maintained in Gutenberg. We don't use it directly today, but anything that imports `@wordpress/admin-ui` from the core script handle will break. +3. **CSS class `.components-notice__action` removed** from the bundled stylesheet. +4. **Menu item height 40px → 32px** (denser dropdowns). +5. **`Button` font-weight changed to 499.** +6. **`DimensionControl` removed.** +7. **`Sandbox` default `allowSameOrigin` is now `false`.** + +So the real audit goal is twofold: +- **(A) Functional safety:** survive the 30 → 32 component breaking changes before submission. +- **(B) Hygiene:** reduce hand-rolled UI in favor of `@wordpress/components`, and adopt WP design tokens (`var(--wp-admin-theme-color)` etc.) so user color schemes apply. This work isn't *forced* by WP 7.0 but it's the right time, and the issue body explicitly asks for it. + +## Reference — WP 7.0 component & CSS changes that matter + +Sources: `data/wp-includes/css/dist/components/style.css` vs `data/wp-includes.6.9.4.bak/...`; Gutenberg `wp/7.0` CHANGELOG (https://github.com/WordPress/gutenberg/blob/wp/7.0/packages/components/CHANGELOG.md). + +### Breaking defaults — components likely already used in our codebase + +| Component | Change | Where we use it | +|---|---|---| +| `BaseControl`, `TextControl`, `TextareaControl`, `SelectControl`, `CheckboxControl`, **`ToggleControl`**, `ToggleGroupControl`, `SearchControl`, `RangeControl`, `ComboboxControl`, `FormTokenField`, `TreeSelect`, `FocalPointPicker` | `__nextHasNoMarginBottom` defaults to **`true`** (32.0.0). Stacked controls no longer have ~24px bottom margin. | `PluginAbilitiesPanel.jsx` (ToggleControl). All other controls are hand-rolled today — but if we adopt them in fixes, this default applies. | +| `Button` | font-weight changed to `499` (30.7.0) | Used in `App.jsx`, `ChatContainer.jsx`. May render slightly lighter. | +| Menu/`DropdownMenu` items | Default item height 40px → 32px (32.0.0) | `ChatInput.jsx` (Dropdown), `MessageItem.jsx` ability list. | +| `Snackbar` | Default timeout shortened | `ChatContainer.jsx` | +| `Notice` | `.components-notice__action` CSS class removed; action button now supports `disabled` and may have both `url` and `onClick` | `App.jsx`, `WebGPUFallback.jsx`, `ChatContainer.jsx`. We don't currently target `.components-notice__action` in our SCSS — safe. | + +### Removed / replaced + +- **`@wordpress/admin-ui`** — bundled in 6.9, not in 7.0. We don't import this — safe. +- **`DimensionControl`** — removed (32.0.0). We don't use it — safe. + +### New that we could adopt + +- `Card` `size` prop now accepts an **object** for per-edge padding (30.8.0). +- `Notice` action button supports `disabled` + can combine `url` and `onClick` (good for the MCP card's "Copy endpoint URL" + "Open docs" pattern). +- `ConfirmDialog` has new `isBusy` prop (useful for tool execution confirmations). +- New CSS variable `--wp-components-color-gray-400`. + +### Design tokens to adopt (independent of WP 7.0 — WordPress has shipped these for years) + +Our `src/extensions/styles/main.scss` contains 62 literal `#2271b1`, 30 literal `#1d2327`, etc. — never `var(--wp-admin-theme-color)`. User color schemes (Modern, Light, Coffee, …) are not respected. WP exposes these on `:root` from `wp-admin/css/forms.css` (7.0) / `common.css` (6.9): + +- `--wp-admin-theme-color` (default `#3858e9` in modern, `#2271b1` legacy) +- `--wp-admin-theme-color-darker-10`, `-darker-20` +- `--wp-admin-border-width-focus` + +## Inventory — our UI surfaces + +Fifteen React surfaces plus three SCSS stylesheets. **Most components already use `@wordpress/components` to a non-trivial degree.** The component-library adoption is in better shape than the issue body suggests. + +| Surface | File | LOC | `@wordpress/components` used | Status | +|---|---|---|---|---| +| Main admin app shell | `src/extensions/App.jsx` | 295 | `Notice`, `Button` | OK | +| Chat sidebar mount (admin bar) | `src/extensions/admin-sidebar.js` + `styles/admin-sidebar.scss` (326 LOC) | 89 | — | DOM wiring, no React UI to migrate | +| Chat sidebar React shell | `src/extensions/components/AdminSidebar.jsx` | 214 | `Button` (after close-button PR) | OK after close-button PR | +| Chat conversation | `src/extensions/components/ChatContainer.jsx` | 993 | `Button`, `Modal`, `Notice`, `Snackbar` | partial; remaining custom areas are the conversation flow itself | +| Chat input bar | `src/extensions/components/ChatInput.jsx` | 368 | `Dropdown`, `Icon` | mostly custom (product-specific input design) | +| Individual message + tool-call card | `src/extensions/components/MessageItem.jsx` | 856 | (none) | **truly bespoke design language** — Perplexity-style chat bubbles. Intentional. | +| Model loader status | `src/extensions/components/ModelStatus.jsx` | 854 | `Button`, `DropdownMenu`, `MenuGroup`, `MenuItem`, `Spinner` (+ `@wordpress/icons`) | already migrated to component primitives; remaining custom is product-specific progress UI | +| Settings tab | `src/extensions/components/SettingsTab.jsx` | 591 | `Button`, `Card`, `CardHeader`, `CardBody`, `SelectControl`, `Notice`, `ToggleControl` | **already heavily migrated**. Minor nits in §2 below. | +| Abilities catalog | `src/extensions/components/AbilityBrowser.jsx` | 385 | `Spinner` | could adopt `Card` for the per-ability tiles | +| Plugin abilities panel | `src/extensions/components/PluginAbilitiesPanel.jsx` | 389 | `Spinner`, `ToggleControl` | OK | +| Ability picker (in chat input) | `src/extensions/components/AbilityPicker.jsx` | 131 | — | custom dropdown | +| File view (tool result) | `src/extensions/components/FileView.jsx` | 80 | — | custom | +| WebGPU fallback | `src/extensions/components/WebGPUFallback.jsx` | 161 | `Notice`, `ExternalLink` | OK | +| Editor sidebar (Gutenberg plugin) | `src/extensions/components/EditorSidebar.jsx` + `styles/editor-sidebar.scss` (246 LOC) | 137 | — | out of scope for this audit (separate surface) | +| Parked | `src/extensions/components/VoiceButton.jsx` | 400 | — | parked per roadmap | + +**Correction note:** an earlier draft of this audit claimed `SettingsTab.jsx` and `ModelStatus.jsx` had zero `@wordpress/components` usage. That was wrong — the inventory script missed multi-line imports (`import {\n` with the brace on its own line). Both files are migrated to a reasonable degree. The v0.13 fix list below was tightened accordingly. + +**SCSS:** +- `src/extensions/styles/main.scss` — **3,599 LOC**, 62× literal `#2271b1`, zero `var(--wp-*)` token usage +- `src/extensions/styles/admin-sidebar.scss` — 326 LOC +- `src/extensions/styles/editor-sidebar.scss` — 246 LOC + +## Per-surface findings + +Screenshots in `docs/audit-assets/wp-7.0-before/`. + +### 1. Main admin page (`agentic-admin`) + +![Main view](audit-assets/wp-7.0-before/01-plugin-main-initial.png) + +| # | Finding | Priority | Recommended fix | +|---|---|---|---| +| 1.1 | Hand-rolled tab strip ("Chat / Abilities / Plugin Abilities" + "Settings" gear button) using `wp-agentic-admin-tab` class | P1 | Use `@wordpress/components` `TabPanel` or `Tabs` (32.0.0). Keeps keyboard nav + a11y for free. | +| 1.2 | "Copy All" + "Clear Chat" buttons at top of chat panel are hand-rolled | P2 | Use `Button` `variant="tertiary"` for "Copy All", `variant="secondary"` for "Clear Chat" (`isDestructive` if confirming). | +| 1.3 | Greeting bubble has an inline timestamp + copy button — custom card | P2 | Acceptable as a custom chat-bubble design (this is the product's identity). No fix. | + +### 2. Settings tab + +**Current state on `dev` (no MCP yet):** +![Settings tab on dev](audit-assets/wp-7.0-before/02b-settings-tab-on-dev.png) + +**Future state after PR #216 merges (includes MCP Endpoint card):** +![Settings tab with MCP](audit-assets/wp-7.0-before/02-settings-tab-full.png) + +The "HTTP 404" visible in the second screenshot is **not a bug** — it was a stale build artifact: a JS bundle built on the `feat/001-mcp-server-endpoint` branch was still on disk when viewing the dev branch (which doesn't have the PHP routes yet). After rebuilding on dev, the MCP card disappears as expected. PR #216 ships both halves together. + +| # | Finding | Priority | Recommended fix | +|---|---|---|---| +| 2.1 | KB progress bar uses hand-rolled `
` with hardcoded `#007cba` | P2 | Replace with `ProgressBar` (stabilised in @wordpress/components 32.0.0) or tokenise the literal. | +| 2.2 | KB status display is a hand-rolled `` with custom class | P2 | Definition-list pattern is fine, or migrate to `__experimentalGrid`. Low priority. | +| 2.3 | "Context Window per Model" + "Remote Provider Context Window" h3 headings sit OUTSIDE any `Card` (KB / GPU Info / Thinking Mode each have a `CardHeader`). Inconsistent grouping. | P2 | Wrap each section's heading in `CardHeader` for consistency, OR pull all section headings out of cards. Cosmetic. | +| 2.4 | "Disable thinking before tool selection" + "Disable thinking after tool results" rendered as two stacked `ToggleControl`s | P2 | These are independent toggles, so `ToggleControl` is correct. Worth verifying spacing in WP 7.0 since `__nextHasNoMarginBottom` defaults to `true` — controls may visually collapse. | +| 2.5 | Inline `style={{...}}` calls scattered across the file | P3 | Move to SCSS. Pure hygiene. | +| 2.6 | MCP Endpoint card (post-#216) has its own custom toggle and "Save MCP settings" button | P1 (post-#216) | Replace custom toggle with `ToggleControl`. Replace Save button with `Button variant="primary"`. Migrate when #216 lands. | + +### 3. Abilities tab + +![Abilities tab](audit-assets/wp-7.0-before/03-abilities-tab.png) + +| # | Finding | Priority | Recommended fix | +|---|---|---|---| +| 3.1 | Two-column card grid is clean and consistent; layout is fine | — | Keep. | +| 3.2 | Each ability card is hand-rolled with title + description + monospace ID + Execute button | P2 | Migrate the card shell to `Card`/`CardHeader`/`CardBody`/`CardFooter`. Keep the monospace ID block as-is (custom). | +| 3.3 | Highlighted (red border) ability cards (e.g. "Clear Log Records", "Deactivate Plugin") | P2 | If marking destructive abilities, set `isDestructive` on the Execute `Button` instead of the card border, or use a `Notice` `status="warning"` inside the card. | +| 3.4 | "Execute" button on every card | P2 | `Button variant="primary"` or `variant="secondary"` consistent with severity. | + +### 4. Plugin Abilities tab (empty state) + +![Plugin Abilities empty state](audit-assets/wp-7.0-before/04-plugin-abilities-tab.png) + +| # | Finding | Priority | Recommended fix | +|---|---|---|---| +| 4.1 | Empty state uses a `Notice` (blue left border) inside a card | — | Looks correct. Keep. | +| 4.2 | Did not capture a populated state (would need a third-party plugin exposing Abilities API) | — | Capture after fixes land, or add a fixture plugin to e2e screenshots. | + +### 5. Admin-bar chat sidebar + +![Admin sidebar overlay](audit-assets/wp-7.0-before/05-admin-sidebar-on-posts.png) + +| # | Finding | Priority | Recommended fix | +|---|---|---|---| +| 5.1 | Right-rail sidebar (~400px) opens cleanly on any wp-admin page | — | Pattern works. Keep. | +| 5.2 | Header ("AI Assistant" + status pill + 3-dot menu) hand-rolled | P2 | `Card` + `__experimentalHStack` for header layout; menu via `DropdownMenu` (note: 32px item height in 7.0). | +| 5.3 | Bottom input area duplicated from main view | P2 | Already shared as `ChatInput.jsx`. Confirm no divergent styling. | +| 5.4 | No close (×) button visible in header — relies on overlay click or Escape | P1 | Add explicit close `Button icon={close}` for discoverability. | + +### 6. Model status / loader (`ModelStatus.jsx`) + +Not captured in isolation (would need to trigger model load from cold). 854 LOC, zero `@wordpress/components`. + +| # | Finding | Priority | Recommended fix | +|---|---|---|---| +| 6.1 | Fully hand-rolled progress UI + model picker + GPU detection display | P1 | This is the largest single hand-rolled surface. Out of scope to rewrite for v0.13 — keep on v0.14 list. | +| 6.2 | Status pill at bottom of every view ("Qwen 3 1.7B (Q4 F16) ready" + PERSISTENT + Context meter) | P2 | The pill itself is fine. Context meter (the inline progress bar) could become `ProgressBar` (32.0.0 stabilised). | + +### 7. SCSS — design tokens + +| # | Finding | Priority | Recommended fix | +|---|---|---|---| +| 7.1 | 62× literal `#2271b1` — accent color hardcoded; user color schemes ignored | P1 | Replace with `var(--wp-admin-theme-color)`. Fall back via `@supports` or just commit; WP has shipped this token since 5.7. | +| 7.2 | Other literal tokenisable colors (`#1d2327` text, `#646970` muted, `#c3c4c7` border, `#f6f7f7` surface) | P2 | These are stable across WP themes and aren't tokenised in core. Acceptable as literals, but move to one `$variables` SCSS partial so they're swappable. | +| 7.3 | No tests for color-scheme override | P2 | Add an e2e check that activates a non-default color scheme (e.g. "Modern") and screenshots the plugin's primary accent. | + +## Prioritized fix list — release split + +### v0.13 (submission gate) — must-do + +These are functional safety items that block a confident WP 7.0 submission. The list narrowed from the initial draft after (a) discovering the "MCP 404" was a stale-build artifact, not a bug, and (b) re-inventorying showed `SettingsTab.jsx` and `ModelStatus.jsx` were already migrated to `@wordpress/components`. + +1. **[P1] Replace 62× `#2271b1` literal with `var(--wp-admin-theme-color)`** in `main.scss` + `admin-sidebar.scss` — done in `feat/220-wp-admin-theme-token` +2. **[P1] Add explicit close button to admin-bar sidebar header** — done in `feat/220-admin-sidebar-close-button` + +Both PRs include before/after screenshots in this audit's `audit-assets/wp-7.0-before/`. + +### v0.14 (post-submission polish) + +3. **[P2] SettingsTab nits**: KB progress bar `
` → `ProgressBar` (or token), wrap Context-Window section heading in `CardHeader` for consistency, move inline styles to SCSS. +4. **[P2] Verify `__nextHasNoMarginBottom = true` default doesn't visually collapse stacked `ToggleControl`s in Thinking Mode** on WP 7.0. +5. **[P2] Migrate MCP Endpoint card to `ToggleControl` + `Button variant="primary"`** — depends on #216 landing first. +6. **[P2] Abilities tab → `Card` primitives** for per-ability tiles. +7. **[P2] Tabs → `TabPanel`/`Tabs`** (keyboard nav + a11y for free). +8. **[P2] Move SCSS literal colors to a `$tokens.scss` partial.** +9. **[P2] Capture populated Plugin Abilities tab state** (add a fixture plugin to e2e). +10. **[P2] ModelStatus.jsx review** — already uses `Button`/`DropdownMenu`/`Spinner`. Remaining custom is the progress UI; treat as its own PR if/when needed. + +## Out of scope + +- **EditorSidebar.jsx** — Gutenberg block-editor side panel. Separate audit when we revisit the editor flow. +- **VoiceButton.jsx** — parked per [v1 roadmap](.claude/plans/...). +- Information architecture changes to the Settings tab (sections, ordering). Issue body explicitly says "Restructuring the Settings IA is a separate issue if needed." + +## Verification done during audit + +- Local docker stack upgraded from WP 6.9.4 → 7.0 (manual core swap, `data/wp-admin.6.9.4.bak/` + `data/wp-includes.6.9.4.bak/` retained for diff) +- WP 7.0 dashboard rendered (`docs/audit-assets/wp-7.0-before/00-wp-dashboard.png`) +- All 4 plugin tabs + admin-bar sidebar captured +- CSS diff: `wp-admin/css/common.css` 6.9.4 → 7.0 — no removed/renamed admin classes +- Component CSS diff: only `.components-notice__action` removed from the bundled stylesheet — we do not target it + +## Screenshot index + +- `00-wp-dashboard.png` — WP 7.0 baseline (no plugin) +- `01-plugin-main-initial.png` — plugin main page, Chat tab +- `02-settings-tab-full.png` — Settings tab as it appears with stale MCP build (post-#216 preview) +- `02b-settings-tab-on-dev.png` — Settings tab as it actually exists on `dev` today +- `03-abilities-tab.png` — Abilities catalog (full page) +- `04-plugin-abilities-tab.png` — Plugin Abilities tab, empty state +- `05-admin-sidebar-on-posts.png` — admin-bar chat sidebar overlaid on Posts list +- `06-after-token-migration.png` — plugin main after `var(--wp-admin-theme-color)` migration, default Fresh scheme (visually identical to baseline) +- `07-token-with-modern-scheme.png` — token migration with Modern color scheme active +- `08-token-with-sunrise-scheme.png` — token migration with Sunrise scheme: orange accents now flow into plugin UI +- `09-admin-sidebar-with-close.png` — admin-bar sidebar after close-button PR (× icon top-right) diff --git a/docs/audit-assets/wp-7.0-before/00-wp-dashboard.png b/docs/audit-assets/wp-7.0-before/00-wp-dashboard.png new file mode 100644 index 0000000..6db7ea1 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/00-wp-dashboard.png differ diff --git a/docs/audit-assets/wp-7.0-before/01-plugin-main-initial.png b/docs/audit-assets/wp-7.0-before/01-plugin-main-initial.png new file mode 100644 index 0000000..72ae410 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/01-plugin-main-initial.png differ diff --git a/docs/audit-assets/wp-7.0-before/02-settings-tab-full.png b/docs/audit-assets/wp-7.0-before/02-settings-tab-full.png new file mode 100644 index 0000000..ba04628 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/02-settings-tab-full.png differ diff --git a/docs/audit-assets/wp-7.0-before/02b-settings-tab-on-dev.png b/docs/audit-assets/wp-7.0-before/02b-settings-tab-on-dev.png new file mode 100644 index 0000000..333f46d Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/02b-settings-tab-on-dev.png differ diff --git a/docs/audit-assets/wp-7.0-before/03-abilities-tab.png b/docs/audit-assets/wp-7.0-before/03-abilities-tab.png new file mode 100644 index 0000000..c843cbc Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/03-abilities-tab.png differ diff --git a/docs/audit-assets/wp-7.0-before/04-plugin-abilities-tab.png b/docs/audit-assets/wp-7.0-before/04-plugin-abilities-tab.png new file mode 100644 index 0000000..ade583a Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/04-plugin-abilities-tab.png differ diff --git a/docs/audit-assets/wp-7.0-before/05-admin-sidebar-on-posts.png b/docs/audit-assets/wp-7.0-before/05-admin-sidebar-on-posts.png new file mode 100644 index 0000000..afd1c9c Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/05-admin-sidebar-on-posts.png differ diff --git a/docs/audit-assets/wp-7.0-before/06-after-token-migration.png b/docs/audit-assets/wp-7.0-before/06-after-token-migration.png new file mode 100644 index 0000000..7ebd12c Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/06-after-token-migration.png differ diff --git a/docs/audit-assets/wp-7.0-before/07-token-with-modern-scheme.png b/docs/audit-assets/wp-7.0-before/07-token-with-modern-scheme.png new file mode 100644 index 0000000..9f08493 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/07-token-with-modern-scheme.png differ diff --git a/docs/audit-assets/wp-7.0-before/08-token-with-sunrise-scheme.png b/docs/audit-assets/wp-7.0-before/08-token-with-sunrise-scheme.png new file mode 100644 index 0000000..9ec1e9b Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/08-token-with-sunrise-scheme.png differ diff --git a/docs/audit-assets/wp-7.0-before/09-admin-sidebar-with-close.png b/docs/audit-assets/wp-7.0-before/09-admin-sidebar-with-close.png new file mode 100644 index 0000000..ea74cc3 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/09-admin-sidebar-with-close.png differ diff --git a/docs/audit-assets/wp-7.0-before/10-observe-close-button.png b/docs/audit-assets/wp-7.0-before/10-observe-close-button.png new file mode 100644 index 0000000..ca6fe49 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/10-observe-close-button.png differ diff --git a/docs/audit-assets/wp-7.0-before/a1-settings-via-tabpanel.png b/docs/audit-assets/wp-7.0-before/a1-settings-via-tabpanel.png new file mode 100644 index 0000000..94262c9 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/a1-settings-via-tabpanel.png differ diff --git a/docs/audit-assets/wp-7.0-before/a1-tabs-tabpanel.png b/docs/audit-assets/wp-7.0-before/a1-tabs-tabpanel.png new file mode 100644 index 0000000..e2f9867 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/a1-tabs-tabpanel.png differ diff --git a/docs/audit-assets/wp-7.0-before/a2-abilities-cards.png b/docs/audit-assets/wp-7.0-before/a2-abilities-cards.png new file mode 100644 index 0000000..016249e Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/a2-abilities-cards.png differ diff --git a/docs/audit-assets/wp-7.0-before/a5-settings-tables.png b/docs/audit-assets/wp-7.0-before/a5-settings-tables.png new file mode 100644 index 0000000..4b22514 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/a5-settings-tables.png differ diff --git a/docs/audit-assets/wp-7.0-before/a6-chat-hstack.png b/docs/audit-assets/wp-7.0-before/a6-chat-hstack.png new file mode 100644 index 0000000..17fa35f Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/a6-chat-hstack.png differ diff --git a/docs/audit-assets/wp-7.0-before/a7-plugin-abilities-panel.png b/docs/audit-assets/wp-7.0-before/a7-plugin-abilities-panel.png new file mode 100644 index 0000000..b2df0f3 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/a7-plugin-abilities-panel.png differ diff --git a/docs/audit-assets/wp-7.0-before/b1-bundle-dropdown.png b/docs/audit-assets/wp-7.0-before/b1-bundle-dropdown.png new file mode 100644 index 0000000..28c6096 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/b1-bundle-dropdown.png differ diff --git a/docs/audit-assets/wp-7.0-before/b1-composer.png b/docs/audit-assets/wp-7.0-before/b1-composer.png new file mode 100644 index 0000000..affdbe6 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/b1-composer.png differ diff --git a/docs/audit-assets/wp-7.0-before/b2-model-status.png b/docs/audit-assets/wp-7.0-before/b2-model-status.png new file mode 100644 index 0000000..9054188 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/b2-model-status.png differ diff --git a/docs/audit-assets/wp-7.0-before/c1-chat-rewrite.png b/docs/audit-assets/wp-7.0-before/c1-chat-rewrite.png new file mode 100644 index 0000000..cb5a1e9 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/c1-chat-rewrite.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-chat-tab.png b/docs/audit-assets/wp-7.0-before/glitch-chat-tab.png new file mode 100644 index 0000000..a493834 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-chat-tab.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-1.png b/docs/audit-assets/wp-7.0-before/glitch-fix-1.png new file mode 100644 index 0000000..e5d58da Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-1.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-abilities-padding.png b/docs/audit-assets/wp-7.0-before/glitch-fix-abilities-padding.png new file mode 100644 index 0000000..e4b65a7 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-abilities-padding.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-abilities.png b/docs/audit-assets/wp-7.0-before/glitch-fix-abilities.png new file mode 100644 index 0000000..addbd26 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-abilities.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius-2.png b/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius-2.png new file mode 100644 index 0000000..cfd6d8b Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius-2.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius-3.png b/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius-3.png new file mode 100644 index 0000000..1c66cbd Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius-3.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius.png b/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius.png new file mode 100644 index 0000000..bc727ff Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-card-radius.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-chatheader-border.png b/docs/audit-assets/wp-7.0-before/glitch-fix-chatheader-border.png new file mode 100644 index 0000000..bb72c96 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-chatheader-border.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-connector-empty.png b/docs/audit-assets/wp-7.0-before/glitch-fix-connector-empty.png new file mode 100644 index 0000000..0560361 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-connector-empty.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-fullwidth.png b/docs/audit-assets/wp-7.0-before/glitch-fix-fullwidth.png new file mode 100644 index 0000000..b06a441 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-fullwidth.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-p-margin.png b/docs/audit-assets/wp-7.0-before/glitch-fix-p-margin.png new file mode 100644 index 0000000..6de13e4 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-p-margin.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-panel-shadow.png b/docs/audit-assets/wp-7.0-before/glitch-fix-panel-shadow.png new file mode 100644 index 0000000..0e65f3e Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-panel-shadow.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-pill-in-toolbar-2.png b/docs/audit-assets/wp-7.0-before/glitch-fix-pill-in-toolbar-2.png new file mode 100644 index 0000000..b38275c Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-pill-in-toolbar-2.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-pill-in-toolbar.png b/docs/audit-assets/wp-7.0-before/glitch-fix-pill-in-toolbar.png new file mode 100644 index 0000000..b38275c Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-pill-in-toolbar.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-pill-left.png b/docs/audit-assets/wp-7.0-before/glitch-fix-pill-left.png new file mode 100644 index 0000000..362047e Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-pill-left.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-plugin-abilities.png b/docs/audit-assets/wp-7.0-before/glitch-fix-plugin-abilities.png new file mode 100644 index 0000000..e4514cf Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-plugin-abilities.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-settings.png b/docs/audit-assets/wp-7.0-before/glitch-fix-settings.png new file mode 100644 index 0000000..35eb783 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-settings.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-2.png b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-2.png new file mode 100644 index 0000000..c7f0a0b Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-2.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-3.png b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-3.png new file mode 100644 index 0000000..c7f0a0b Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-3.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-4.png b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-4.png new file mode 100644 index 0000000..2862a15 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar-4.png differ diff --git a/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar.png b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar.png new file mode 100644 index 0000000..cbeef22 Binary files /dev/null and b/docs/audit-assets/wp-7.0-before/glitch-fix-sidebar.png differ diff --git a/includes/class-admin-bar.php b/includes/class-admin-bar.php deleted file mode 100644 index 69b62b9..0000000 --- a/includes/class-admin-bar.php +++ /dev/null @@ -1,156 +0,0 @@ -add_node( - array( - 'id' => 'wp-agentic-admin-sidebar-toggle', - 'parent' => 'top-secondary', - 'title' => '' . esc_html__( 'AI Assistant', 'agentic-admin' ) . '', - 'href' => '#', - 'meta' => array( - 'class' => 'wp-agentic-admin-toggle', - 'title' => __( 'AI Assistant', 'agentic-admin' ), - ), - ) - ); - } - - /** - * Enqueue sidebar scripts and styles on all admin pages. - * - * @param string $hook The current admin page hook. - */ - public function enqueue_scripts( string $hook ): void { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - // Skip on the plugin's own settings page — the full app is already loaded. - if ( 'toplevel_page_agentic-admin' === $hook ) { - return; - } - - $asset_file = WP_AGENTIC_ADMIN_PLUGIN_DIR . 'build-extensions/admin-sidebar.asset.php'; - - if ( ! file_exists( $asset_file ) ) { - return; - } - - $asset = require $asset_file; - $deps = isset( $asset['dependencies'] ) ? (array) $asset['dependencies'] : array( 'wp-element' ); - $ver = isset( $asset['version'] ) ? $asset['version'] : WP_AGENTIC_ADMIN_VERSION; - - // Enqueue WordPress components styles. - wp_enqueue_style( 'wp-components' ); - - // Enqueue sidebar styles. - $css_file = WP_AGENTIC_ADMIN_PLUGIN_DIR . 'build-extensions/admin-sidebar.css'; - if ( file_exists( $css_file ) ) { - wp_enqueue_style( - 'wp-agentic-admin-sidebar-style', - WP_AGENTIC_ADMIN_PLUGIN_URL . 'build-extensions/admin-sidebar.css', - array( 'wp-components', 'dashicons' ), - filemtime( $css_file ) - ); - } - - // Register and enqueue sidebar script. - wp_register_script( - 'wp-agentic-admin-sidebar', - WP_AGENTIC_ADMIN_PLUGIN_URL . 'build-extensions/admin-sidebar.js', - $deps, - $ver, - true - ); - - // Localize with the same data as the admin page. - wp_localize_script( - 'wp-agentic-admin-sidebar', - 'wpAgenticAdmin', - Admin_Page::get_localized_data() - ); - - wp_enqueue_script( 'wp-agentic-admin-sidebar' ); - } - - /** - * Render the sidebar container in the admin footer. - */ - public function render_sidebar_container(): void { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - // Don't render on the plugin page — the full app handles it. - $screen = get_current_screen(); - if ( $screen && 'toplevel_page_agentic-admin' === $screen->id ) { - return; - } - ?> -
-
- esc_url_raw( rest_url( 'wp-abilities/v1' ) ), + 'restRoot' => esc_url_raw( rest_url() ), + 'connectorsRestUrl' => esc_url_raw( rest_url( 'wp-agentic-admin/v1/connectors' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'userId' => get_current_user_id(), 'pluginUrl' => esc_url( WP_AGENTIC_ADMIN_PLUGIN_URL ), diff --git a/includes/class-connectors.php b/includes/class-connectors.php new file mode 100644 index 0000000..4c78a96 --- /dev/null +++ b/includes/class-connectors.php @@ -0,0 +1,459 @@ + 'GET', + 'callback' => array( static::class, 'list_connectors' ), + 'permission_callback' => array( static::class, 'check_permission' ), + ) + ); + + \register_rest_route( + 'wp-agentic-admin/v1', + '/connectors/chat/completions', + array( + 'methods' => 'POST', + 'callback' => array( static::class, 'chat_completion' ), + 'permission_callback' => array( static::class, 'check_permission' ), + 'args' => array( + 'connector_id' => array( + 'required' => true, + 'type' => 'string', + ), + 'model_id' => array( + 'required' => false, + 'type' => 'string', + 'default' => '', + ), + 'messages' => array( + 'required' => true, + 'type' => 'array', + ), + ), + ) + ); + } + + /** + * Admin-only permission check. + * + * @return bool + */ + public static function check_permission(): bool { + return \current_user_can( 'manage_options' ); + } + + /** + * GET /v1/connectors — list AI provider connectors. + * + * @return \WP_REST_Response + */ + public static function list_connectors(): \WP_REST_Response { + $out = array( + 'wp_supports_connectors' => function_exists( '\\wp_get_connectors' ), + 'options_url' => \admin_url( 'options-connectors.php' ), + 'connectors' => array(), + ); + + if ( ! function_exists( '\\wp_get_connectors' ) ) { + return new \WP_REST_Response( $out, 200 ); + } + + $all = \wp_get_connectors(); + $ai_registry = null; + if ( class_exists( '\\WordPress\\AiClient\\AiClient' ) ) { + try { + $ai_registry = \WordPress\AiClient\AiClient::defaultRegistry(); + } catch ( \Exception $e ) { + $ai_registry = null; + } + } + + foreach ( $all as $id => $data ) { + if ( ! isset( $data['type'] ) || 'ai_provider' !== $data['type'] ) { + continue; + } + + $is_connected = false; + if ( null !== $ai_registry ) { + try { + $is_connected = $ai_registry->hasProvider( $id ) + && $ai_registry->isProviderConfigured( $id ); + } catch ( \Exception $e ) { + $is_connected = false; + } + } + + $models = array(); + if ( $is_connected && null !== $ai_registry ) { + try { + $provider_class = $ai_registry->getProviderClassName( $id ); + if ( $provider_class && method_exists( $provider_class, 'modelMetadataDirectory' ) ) { + $directory = $provider_class::modelMetadataDirectory(); + foreach ( $directory->listModelMetadata() as $meta ) { + $models[] = array( + 'id' => $meta->getId(), + 'name' => $meta->getName(), + ); + } + } + } catch ( \Exception $e ) { + $models = array(); + } + } + + $out['connectors'][] = array( + 'id' => $id, + 'name' => $data['name'] ?? $id, + 'description' => $data['description'] ?? '', + 'logo_url' => $data['logo_url'] ?? null, + 'is_connected' => $is_connected, + 'models' => $models, + ); + } + + return new \WP_REST_Response( $out, 200 ); + } + + /** + * POST /v1/connectors/chat/completions — proxy a chat completion through + * the AI Client to a configured connector's provider. + * + * KNOWN LIMITATIONS (documented for honesty, will be addressed in follow-up): + * - Non-streaming only. AI Client doesn't expose a streaming API for + * text generation, so this returns the full assistant message at once. + * - No tool/function calling support yet. + * + * Guards: enforces MAX_MESSAGES, MAX_TOTAL_CHARS, and a per-user + * RATE_LIMIT_PER_MINUTE via a transient. These cap potential paid- + * provider spend triggered through this endpoint. + * + * @param \WP_REST_Request $request REST request. + * @return \WP_REST_Response|\WP_Error + */ + public static function chat_completion( \WP_REST_Request $request ) { + if ( ! class_exists( '\\WordPress\\AiClient\\AiClient' ) ) { + return new \WP_Error( + 'wpaa_no_ai_client', + 'WP AI Client is not available (requires WordPress 7.0+).', + array( 'status' => 501 ) + ); + } + + $connector_id = (string) $request->get_param( 'connector_id' ); + $model_id = (string) $request->get_param( 'model_id' ); + $messages = (array) $request->get_param( 'messages' ); + + if ( '' === $connector_id || empty( $messages ) ) { + return new \WP_Error( + 'wpaa_bad_request', + 'connector_id and messages are required.', + array( 'status' => 400 ) + ); + } + + if ( count( $messages ) > self::MAX_MESSAGES ) { + return new \WP_Error( + 'wpaa_too_many_messages', + sprintf( 'Too many messages: limit is %d per request.', self::MAX_MESSAGES ), + array( 'status' => 413 ) + ); + } + + $total_chars = 0; + foreach ( $messages as $msg ) { + if ( is_array( $msg ) && isset( $msg['content'] ) ) { + $total_chars += strlen( (string) $msg['content'] ); + } + } + if ( $total_chars > self::MAX_TOTAL_CHARS ) { + return new \WP_Error( + 'wpaa_payload_too_large', + sprintf( 'Combined message content exceeds %d characters.', self::MAX_TOTAL_CHARS ), + array( 'status' => 413 ) + ); + } + + $rate_limit_error = self::check_rate_limit(); + if ( null !== $rate_limit_error ) { + return $rate_limit_error; + } + + try { + $registry = \WordPress\AiClient\AiClient::defaultRegistry(); + if ( ! $registry->hasProvider( $connector_id ) ) { + return new \WP_Error( + 'wpaa_unknown_connector', + sprintf( 'Connector "%s" is not registered with the AI Client.', $connector_id ), + array( 'status' => 404 ) + ); + } + if ( ! $registry->isProviderConfigured( $connector_id ) ) { + return new \WP_Error( + 'wpaa_unconfigured_connector', + sprintf( 'Connector "%s" is not configured (missing API key).', $connector_id ), + array( 'status' => 400 ) + ); + } + + // Resolve a specific model if requested; fall back to AI Client + // auto-discovery if none provided or resolution fails. + $model = null; + if ( '' !== $model_id ) { + try { + $provider = $registry->getProviderModel( $connector_id, $model_id ); + $model = $provider; + } catch ( \Exception $e ) { + $model = null; + } + } + + $result = self::generate_with_structured_messages( $messages, $model ); + $text = method_exists( $result, 'toText' ) ? $result->toText() : (string) $result; + + // Return an OpenAI-shaped non-streaming response so the existing + // chat orchestrator code path can consume it without changes. + $response = array( + 'id' => 'chatcmpl-' . wp_generate_uuid4(), + 'object' => 'chat.completion', + 'created' => time(), + 'model' => $connector_id, + 'choices' => array( + array( + 'index' => 0, + 'message' => array( + 'role' => 'assistant', + 'content' => $text, + ), + 'finish_reason' => 'stop', + ), + ), + ); + + return new \WP_REST_Response( $response, 200 ); + } catch ( \Exception $e ) { + return new \WP_Error( + 'wpaa_connector_error', + $e->getMessage(), + array( 'status' => 500 ) + ); + } + } + + /** + * Generate a text result through the AI Client using structured + * roles so the connector can preserve user / assistant turns and + * pass any system instruction as a top-level model directive. + * + * Falls back to a single concatenated prompt only if the AI Client + * structural builder isn't available at runtime. + * + * @param array> $messages OpenAI-style chat messages. + * @param mixed|null $model Optional resolved AI Client model. + * @return mixed The generated AI Client text result. + */ + private static function generate_with_structured_messages( array $messages, $model ) { + $system_instructions = array(); + $turns = array(); + + foreach ( $messages as $msg ) { + if ( ! is_array( $msg ) || ! isset( $msg['content'] ) ) { + continue; + } + $role = isset( $msg['role'] ) ? (string) $msg['role'] : 'user'; + $content = (string) $msg['content']; + + if ( 'system' === $role ) { + $system_instructions[] = $content; + continue; + } + $turns[] = array( + 'role' => $role, + 'content' => $content, + ); + } + + // History must start with a user turn (most providers — Anthropic + // in particular — reject leading assistant turns). Drop synthetic + // assistant-led messages like the welcome card until the first + // real user message. + while ( ! empty( $turns ) && 'user' !== $turns[0]['role'] ) { + array_shift( $turns ); + } + + $structural_supported = class_exists( '\\WordPress\\AiClient\\AiClient' ) + && class_exists( '\\WordPress\\AiClient\\Messages\\DTO\\Message' ) + && class_exists( '\\WordPress\\AiClient\\Messages\\DTO\\MessagePart' ) + && class_exists( '\\WordPress\\AiClient\\Messages\\Enums\\MessageRoleEnum' ); + + if ( ! $structural_supported || empty( $turns ) ) { + // Final fallback: lossy concatenation. Only reached when the + // AI Client builder classes are missing or the message list + // had no user/assistant turns at all. + $prompt = self::flatten_messages_lossy( + array_merge( + array_map( + static function ( $s ) { + return array( + 'role' => 'system', + 'content' => $s, + ); + }, + $system_instructions + ), + $turns + ) + ); + return null === $model + ? \WordPress\AiClient\AiClient::generateTextResult( $prompt ) + : \WordPress\AiClient\AiClient::generateTextResult( $prompt, $model ); + } + + $role_enum = '\\WordPress\\AiClient\\Messages\\Enums\\MessageRoleEnum'; + $messages_dto = array(); + foreach ( $turns as $turn ) { + $enum_role = 'assistant' === $turn['role'] + ? $role_enum::model() + : $role_enum::user(); + $messages_dto[] = new \WordPress\AiClient\Messages\DTO\Message( + $enum_role, + array( new \WordPress\AiClient\Messages\DTO\MessagePart( $turn['content'] ) ) + ); + } + + // Use the latest turn as the active prompt and pass the rest as history, + // matching PromptBuilder's expected shape. + $latest = array_pop( $messages_dto ); + $history = $messages_dto; + + $builder = \WordPress\AiClient\AiClient::prompt(); + foreach ( $latest->getParts() as $part ) { + $builder = $builder->withMessageParts( $part ); + } + if ( ! empty( $history ) ) { + $builder = $builder->withHistory( ...$history ); + } + if ( ! empty( $system_instructions ) ) { + $builder = $builder->usingSystemInstruction( + implode( "\n\n", $system_instructions ) + ); + } + if ( null !== $model ) { + $builder = $builder->usingModel( $model ); + } + + return $builder->generateTextResult(); + } + + /** + * Concatenate messages into a single prompt as a last-resort fallback + * when the structural Message DTOs are unavailable. + * + * @param array $messages OpenAI-style chat messages. + * @return string + */ + private static function flatten_messages_lossy( array $messages ): string { + $lines = array(); + foreach ( $messages as $msg ) { + if ( ! is_array( $msg ) || ! isset( $msg['content'] ) ) { + continue; + } + $role = isset( $msg['role'] ) ? ucfirst( (string) $msg['role'] ) : 'User'; + $lines[] = $role . ': ' . (string) $msg['content']; + } + return implode( "\n\n", $lines ); + } + + /** + * Per-user rate limit for chat completions. + * + * Caps at RATE_LIMIT_PER_MINUTE requests per user per rolling 60s + * window via a transient counter. Returns a WP_Error 429 when the + * cap is exceeded, null otherwise. + * + * @return \WP_Error|null + */ + private static function check_rate_limit(): ?\WP_Error { + $user_id = \get_current_user_id(); + if ( ! $user_id ) { + return null; + } + + $key = 'wpaa_conn_rl_' . $user_id; + $count = (int) \get_transient( $key ); + + if ( $count >= self::RATE_LIMIT_PER_MINUTE ) { + return new \WP_Error( + 'wpaa_rate_limited', + sprintf( + 'Connector chat completions are rate-limited to %d requests per minute.', + self::RATE_LIMIT_PER_MINUTE + ), + array( 'status' => 429 ) + ); + } + + \set_transient( $key, $count + 1, MINUTE_IN_SECONDS ); + return null; + } +} diff --git a/includes/class-editor-sidebar.php b/includes/class-editor-sidebar.php deleted file mode 100644 index 9a7b1dd..0000000 --- a/includes/class-editor-sidebar.php +++ /dev/null @@ -1,103 +0,0 @@ -=10" } }, + "node_modules/@wordpress/hooks": { + "version": "4.46.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.46.0.tgz", + "integrity": "sha512-fsKw4dmw4voIRoKc8t0XRREQlFvwj9XS/jTXvkh6mqRYCDpaEnrdB2Ji5jgbRXEMPU0GKVGMeAn5Wwi56gjBMg==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/icons": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-12.0.0.tgz", diff --git a/package.json b/package.json index 8b4582c..4384341 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "export:feedback": "bash scripts/export-feedback.sh" }, "devDependencies": { + "@wordpress/hooks": "^4.46.0", "@wordpress/scripts": "^31.0.0", "del-cli": "^5.1.0" }, diff --git a/readme.txt b/readme.txt index 55d567d..c1390e9 100644 --- a/readme.txt +++ b/readme.txt @@ -1,8 +1,8 @@ -=== Agentic Admin for WordPress === +=== Agentic Admin === Contributors: pluginslab Tags: ai, sre, site reliability, webllm, abilities api Requires at least: 6.9 -Tested up to: 6.9 +Tested up to: 7.0 Requires PHP: 8.2 Stable tag: 0.11.0 License: GPL-2.0-or-later @@ -12,7 +12,7 @@ A privacy-first AI Site Reliability Engineer running entirely in the browser via == Description == -Agentic Admin for WordPress transforms your WordPress admin panel into an intelligent command center. Instead of navigating through multiple screens to diagnose issues, you simply describe your problem in plain English. +Agentic Admin transforms your WordPress admin panel into an intelligent command center. Instead of navigating through multiple screens to diagnose issues, you simply describe your problem in plain English. = Features = @@ -36,10 +36,50 @@ Agentic Admin for WordPress transforms your WordPress admin panel into an intell 4. Wait for the AI model to download (one-time, ~1.2GB for Qwen 3 1.7B or ~4.5GB for Qwen 2.5 7B) 5. Start chatting! +== Screenshots == + +1. The Agentic Admin chat tab in wp-admin, mid-conversation. The model has just answered a question about installed plugins by calling the `plugin-list` tool locally — full ReAct trace (user question, thought process, tool call, answer) visible. +2. First-run model download in progress. The Qwen 3 1.7B weights (~1.2 GB) are fetched from the MLC-AI / HuggingFace CDN — once per browser, cancellable, cached for subsequent sessions. +3. The Abilities browser, listing every tool the assistant can call against the WordPress Abilities API on this site. +4. Settings panel. Build the local knowledge base, see detected GPU + VRAM, tune context-window size per model based on your hardware, toggle thinking mode, and switch between the local engine (WebLLM + WebGPU), a remote OpenAI-compatible endpoint, or the WordPress 7.0 Connector. +5. Multi-step workflow execution. "Do a performance check" is recognized as a 2-step workflow — the assistant runs `site-health` and `error-log-read` in sequence, then summarizes the environment (WP version, PHP, memory, debug mode, error log status) in one answer. +6. WordPress 7.0 AI Connector integration. The Connector tab picks up any AI provider registered via WP 7.0's built-in Connector API — Anthropic, Google, OpenAI, or any third-party `ai_provider` plugin — and uses it as the model backend with zero extra setup. + +== External services == + +This plugin runs AI locally in your browser by default. No prompts or admin data are sent to any server unless you explicitly enable the external LLM provider. The following external services are contacted under specific, disclosed conditions: + +**Model weights CDN (MLC-AI / HuggingFace)** — Always for the local engine. +On first use the browser downloads the selected model (Qwen 3 1.7B by default, ~1.2 GB) from `https://huggingface.co/mlc-ai/` and `https://raw.githubusercontent.com/mlc-ai/binary-mlc-llm-libs/`. Only HTTP GET requests for static model files are made; no prompts, admin data, or telemetry are sent. Weights are cached in the browser; subsequent sessions are offline. +HuggingFace terms: https://huggingface.co/terms-of-service — Privacy: https://huggingface.co/privacy + +**Transformers.js CDN (jsDelivr)** — Only when the local knowledge base is enabled in settings. +The embedding model used to index documentation is loaded from `https://cdn.jsdelivr.net/npm/@xenova/transformers@3.8.1/`. Only the JS bundle and embedding model files are fetched; no prompts or admin data are sent. +jsDelivr terms: https://www.jsdelivr.com/terms — Privacy: https://www.jsdelivr.com/privacy-policy-jsdelivr-net + +**External LLM provider (user-configured)** — Only when you switch the engine from "Local" to "Remote" in settings. +When enabled, chat messages, tool descriptions, and tool results are sent through this plugin's REST proxy (`/wp-json/wp-agentic-admin/v1/llm-proxy/`) to the OpenAI-compatible endpoint URL you configure (e.g. Ollama, LM Studio, vLLM, OpenAI, Groq, Together). You choose the endpoint; the plugin does not preselect or default to any third-party provider. No data is sent until you save an endpoint and start a chat in Remote mode. + +**DuckDuckGo HTML search** — Only when the optional `web-search` ability is invoked by the assistant. +The user's search query is sent to `https://html.duckduckgo.com/html/` over GET. No WordPress user data is sent. +DuckDuckGo terms: https://duckduckgo.com/terms — Privacy: https://duckduckgo.com/privacy + +**NVD CVE database (NIST)** — Only when the optional `plugin-vulnerability-scan` ability is invoked. +Installed plugin names and versions are sent to `https://services.nvd.nist.gov/rest/json/cves/2.0` to check for known CVEs. +NVD terms: https://nvd.nist.gov/general/terms-of-use — Privacy: https://www.nist.gov/privacy-policy + +**MITRE CVE API** — Only when the optional `plugin-vulnerability-scan` ability follows up on a CVE identifier. +The CVE ID (e.g. `CVE-2024-12345`) is sent to `https://cveawg.mitre.org/api/cve/` for details. No WordPress user data is sent. +MITRE terms: https://www.cve.org/Legal/TermsOfUse — Privacy: https://www.cve.org/Legal/PrivacyPolicy + +**WordPress.org plugin checksums** — Only when the optional `verify-plugin-checksums` ability is invoked. +Installed plugin slugs and versions are sent to `https://downloads.wordpress.org/plugin-checksums/` and `https://plugins.svn.wordpress.org/` to verify file integrity against the official WordPress.org distribution. +WordPress.org policies: https://wordpress.org/about/privacy/ + == Changelog == = 0.11.0 = -* Renamed: Plugin is now "Agentic Admin for WordPress". Text domain "agentic-admin", function prefix agentic_admin_*. WordPress.org submission-ready. +* Renamed: Plugin is now "Agentic Admin". Text domain "agentic-admin", function prefix agentic_admin_*. WordPress.org submission-ready. * Removed: feedback system, WebMCP bridge, voice input, and three low-value abilities (backup-check, opcode-cache-status, disk-usage). Code preserved in git history; voice + write-file + content-generate + plugin-ecosystem abilities parked for v1.x as opt-in via WP_AGENTIC_ADMIN_ENABLE_LABS constant. * Security: blocked sensitive-column reads (user_email, user_pass) in query-database to prevent reconnaissance attacks (#166). Hardened query length cap and read-only verb gate. * Security: escaped output in functions-abilities.php (#121). Added sw-loader.php access-control rationale (#123). Documented direct-DB-call rationale in db-optimize and database-check. diff --git a/src/extensions/App.jsx b/src/extensions/App.jsx index 316f17f..cbed631 100644 --- a/src/extensions/App.jsx +++ b/src/extensions/App.jsx @@ -4,7 +4,7 @@ */ import { useState, useEffect, useCallback } from '@wordpress/element'; -import { Notice, Button } from '@wordpress/components'; +import { Notice, TabPanel } from '@wordpress/components'; import { cog } from '@wordpress/icons'; import ChatContainer from './components/ChatContainer'; import AbilityBrowser from './components/AbilityBrowser'; @@ -21,10 +21,6 @@ const App = () => { const [ modelReady, setModelReady ] = useState( false ); const [ webGPUError, setWebGPUError ] = useState( null ); const [ isExecuting, setIsExecuting ] = useState( false ); - // Active view — 'chat' | 'abilities' | 'plugin-abilities' | 'settings'. - // Settings is just another tab (positioned visually on the right via CSS). - const [ activeView, setActiveView ] = useState( 'chat' ); - // Track initialization phase: 'checking' during initial checks, 'loading' when auto-loading, null when done const [ initPhase, setInitPhase ] = useState( 'checking' ); const [ initMessage, setInitMessage ] = useState( 'Checking WebGPU support...' @@ -100,6 +96,38 @@ const App = () => { return; } + if ( savedProvider === 'connector' ) { + const connectorId = localStorage.getItem( + 'agentic_admin_connector_id' + ); + const connectorModel = + localStorage.getItem( + 'agentic_admin_connector_model' + ) || ''; + if ( connectorId ) { + log.info( + 'Connector provider saved, auto-connecting...' + ); + setInitPhase( 'loading' ); + setInitMessage( 'Connecting to connector...' ); + setInitProgress( 35 ); + try { + await modelLoader.loadConnector( + connectorId, + connectorModel + ); + setModelReady( true ); + } catch ( loadErr ) { + log.error( + 'Auto-connect connector failed:', + loadErr + ); + } + } + setInitPhase( null ); + return; + } + // Check if local model is cached setInitMessage( 'Checking cache...' ); setInitProgress( 30 ); @@ -191,27 +219,16 @@ const App = () => { ); } - /** - * Tab panel configuration - */ const tabs = [ - { - name: 'chat', - title: 'Chat', - }, + { name: 'chat', title: 'Chat' }, { name: 'abilities', title: 'Abilities' }, { name: 'plugin-abilities', title: 'Plugin Abilities' }, + { name: 'settings', title: 'Settings', icon: cog }, ]; - /** - * Render content for the active view. - * - * @return {JSX.Element} The rendered view. - */ - const renderActiveView = () => { - switch ( activeView ) { + const renderTab = ( tab ) => { + switch ( tab.name ) { case 'chat': - // If WebGPU has a fatal error, show fallback if ( webGPUError && ! modelReady ) { return ( { return (
-
-
-
- { tabs.map( ( tab ) => ( - - ) ) } - -
-
- { renderActiveView() } -
-
-
+ + { renderTab } + { } ); } + const closeSidebar = ( returnFocus = true ) => { + container.classList.remove( 'is-open' ); + if ( toggleBtn ) { + toggleBtn.classList.remove( 'is-active' ); + const toggleLink = toggleBtn.querySelector( 'a' ) || toggleBtn; + toggleLink.setAttribute( 'aria-expanded', 'false' ); + if ( returnFocus ) { + toggleLink.focus(); + } + } + }; + // Close sidebar when clicking the overlay. const overlay = document.getElementById( 'wp-agentic-admin-sidebar-overlay' ); if ( overlay ) { - overlay.addEventListener( 'click', () => { - container.classList.remove( 'is-open' ); - if ( toggleBtn ) { - toggleBtn.classList.remove( 'is-active' ); - const toggleLink = toggleBtn.querySelector( 'a' ) || toggleBtn; - toggleLink.setAttribute( 'aria-expanded', 'false' ); - } - } ); + overlay.addEventListener( 'click', () => closeSidebar( false ) ); } // Close sidebar on Escape key and return focus to toggle button. document.addEventListener( 'keydown', ( e ) => { if ( e.key === 'Escape' && container.classList.contains( 'is-open' ) ) { - container.classList.remove( 'is-open' ); - if ( toggleBtn ) { - toggleBtn.classList.remove( 'is-active' ); - const toggleLink = toggleBtn.querySelector( 'a' ) || toggleBtn; - toggleLink.setAttribute( 'aria-expanded', 'false' ); - toggleLink.focus(); - } + closeSidebar(); } } ); + // Close sidebar from React (in-panel close button dispatches this). + window.addEventListener( 'wp-agentic-admin/sidebar-close', () => + closeSidebar() + ); + const root = createRoot( container ); root.render( ); diff --git a/src/extensions/components/AbilityBrowser.jsx b/src/extensions/components/AbilityBrowser.jsx index 7ffe228..cd870b8 100644 --- a/src/extensions/components/AbilityBrowser.jsx +++ b/src/extensions/components/AbilityBrowser.jsx @@ -2,22 +2,23 @@ * AbilityBrowser Component * * Manual ability testing interface - browse and execute abilities directly. - * */ import { useState, useEffect } from '@wordpress/element'; -import { Spinner } from '@wordpress/components'; +import { + Button, + Card, + CardHeader, + CardBody, + CardFooter, + Notice, + Spinner, + TextareaControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; import abilitiesApi from '../services/abilities-api'; -/** - * AbilityCard component - displays a single ability - * - * @param {Object} props - Component props - * @param {Object} props.ability - Ability object - * @param {Function} props.onExecute - Callback when execute is clicked - * @param {boolean} props.isExecuting - Whether this ability is currently executing - * @return {JSX.Element} Rendered ability card - */ const AbilityCard = ( { ability, onExecute, isExecuting } ) => { const [ inputJson, setInputJson ] = useState( '{}' ); const [ showInput, setShowInput ] = useState( false ); @@ -28,9 +29,6 @@ const AbilityCard = ( { ability, onExecute, isExecuting } ) => { ability.input_schema?.properties && Object.keys( ability.input_schema.properties ).length > 0; - /** - * Handle execute button click - */ const handleExecute = () => { setInputError( null ); @@ -44,7 +42,6 @@ const AbilityCard = ( { ability, onExecute, isExecuting } ) => { } } - // For destructive abilities, confirm first if ( isDestructive ) { // eslint-disable-next-line no-alert -- intentional confirmation for destructive abilities const confirmed = window.confirm( @@ -58,9 +55,6 @@ const AbilityCard = ( { ability, onExecute, isExecuting } ) => { onExecute( ability, parsedInput ); }; - /** - * Render input schema as helpful hints - */ const renderSchemaHints = () => { if ( ! ability.input_schema?.properties ) { return null; @@ -68,118 +62,98 @@ const AbilityCard = ( { ability, onExecute, isExecuting } ) => { const props = ability.input_schema.properties; return ( -
- Parameters: -
    - { Object.entries( props ).map( ( [ key, schema ] ) => ( -
  • - { key } - { schema.type && ({ schema.type }) } - { schema.default !== undefined && ( - - { ' ' } - = { JSON.stringify( schema.default ) } - - ) } - { schema.description && ( - - { schema.description } - ) } -
  • - ) ) } -
-
+ +
Parameters
+ { Object.entries( props ).map( ( [ key, schema ] ) => ( +
+ { key } + { schema.type && ` (${ schema.type })` } + { schema.default !== undefined && + ` = ${ JSON.stringify( schema.default ) }` } + { schema.description && ` — ${ schema.description }` } +
+ ) ) } +
); }; return ( -
-
-

{ ability.label }

- { isDestructive && ( - - Destructive - - ) } -
- -

- { ability.description } -

- -
- { ability.name } -
- - { hasInputSchema && ( - <> - - - { showInput && ( -
- { renderSchemaHints() } -