Recent Activities
This page shows what are we working on.
-
head
-
Merge pull request #105 from swissup/fix/fuzzy-content-strategy-head-corruption
fix(fuzzy): guard against <head> corruption in FuzzyContentStrategy (#104) ac1fae -
fix(fuzzy): guard against <head> corruption in FuzzyContentStrategy
Two bugs in apply() caused a last-resort patch to replace a <meta>
tag in <head> with a <body> element (e.g. <img>) when the patched
node's attribute value (alt, src, class ≥ 10 chars) appeared earlier
inside a head meta description's content="..." attribute.
Fixes #104 (radiatorkopen.nl: meta description overwritten by <img>).
Changes:
- Restrict strpos search to the <body> substring; <head> content is
never a valid match target for body-element patches.
- After expanding to the surrounding element, reject the match if the
element tag name differs from the patched node's tag name
(<meta> ≠ <img> → continue to next candidate).
Tests:
- testDoesNotReplaceHeadMetaWhenAltTextAppearsInMetaDescriptionFirst
reproduces the exact radiatorkopen.nl scenario (was failing).
- testReturnsNullWhenOnlyMatchIsInsideHead covers the variant where
the only document match is inside <head> (must return null). 772112
-
-
1.18.4
-
Version 1.18.4
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> aa4b80 -
Merge pull request #102 from swissup/fix/requirejs-bundle-pool-order-27428
fix(defer): keep the bundle pool and our RequireJS patches in place 7aeb97 -
test: repair the typo'd DOCTYPE in the offset fixture
`<!` followed by a space is not a doctype, so libxml parsed it as a bogus
comment. With LIBXML_HTML_NOIMPLIED the parser then had no document element to
attach the real <html> to, emitting "misplaced <html> tag"; Magento's unit
bootstrap promotes that warning to an exception, so the test errored on every
run instead of asserting anything.
Also close the space inside the require.js src in the same fixture. That one did
not fail the test — the assertions only count //script[@src] and compare their
offsets — but the path was meaningless as written.
The three other fixtures in this file already spell the doctype correctly.
Fixes #103
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> cdbf37 -
fix(defer): narrow the RequireJS patch guard to override.js
The bootstrap guard matched the whole Swissup_Pagespeed/js/lib/requirejs/
directory, which pinned preload-all-scripts.js in <head> as well. That file is
not bootstrap: addPreloadAllScripts() add()s it at the end of the asset
collection rather than inserting it after the min resolver, and its entire body
runs inside a window load handler, so it cannot race require.config(). Guarding
it only costs a render-blocking head script on stores with Force RequireJS
Loading enabled.
Match 'Swissup_Pagespeed/js/lib/requirejs/override' instead, and correct the
comment: it described insert()-after-min-resolver placement that applies to
override.js alone.
testPreloadAllScriptsStaysOptimizable pins the reverse direction - the file must
still move to body end and take async past the counter threshold. It fails with
the directory-wide prefix and passes with the narrowed one.
Also assert asset presence while collecting positions in
testLiveHeadKeepsBootstrapChainInOrder: strpos returns false for a dropped
asset, which asort() sorts first, reporting a bogus ordering violation instead
of the missing script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 5e0e10 -
fix(defer): keep the bundle pool and our RequireJS patches in place
The 1.18.2 bootstrap guard was meant to cover the bundled module pool, but
'js/bundle/bundle' only matches Magento's own bundle0.js. Advanced JS Bundling
names its pool after the layout handle -- default-bundle0.js,
cms-page-view-bundle0.js -- so none of our bundles ever matched, and
Swissup_Pagespeed/js/lib/requirejs/override.js was not listed at all.
Combined with the 1.18.2 counter change, where skipped-but-executable scripts
consume a slot, these scripts crossed the jsCounter > 5 threshold and started
getting async -- something that did not happen before 1.18.2. Both call
require.config(): the bundle pool registers the jsbuild sources, and override.js
wraps require.config() and must be installed before requirejs-config calls it.
So on a store running Advanced JS Bundling or Interactive Defer, 1.18.3
introduced a fresh instance of the very failure class it fixed.
Match the whole js/bundle/ directory instead of a filename prefix (anything
living there is a module pool by construction) and add our own RequireJS patch
directory.
Regression coverage uses the head sequence dev2.dlastore.com actually serves,
with both features on: six ignored third-party inline blocks, the global require
config, then Magento's bootstrap interleaved with the bundle pool. The fixture
asserts the whole chain stays ahead of the body content, carries no async/defer,
and keeps its document order. It fails in seven places without the fix.
Refs TTF-27428
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 1b477c
-
-
head
-
Version 1.7.37 264578
-
fix: quick view popup silenced by the 1.7.35 gating guard
is-ajaxpro-request's "trust a section load only after a recent ajaxpro
POST" guard (048361e, 1.7.35) only recognized add-to-cart as a plain
GET customer/section/load preceded by a POST. Quick view has no such
POST — it fetches ajaxpro-product directly — so ModalManager never
opened the popup: the request succeeded, the section arrived, nothing
rendered, no error.
Have quick-view declare the request explicitly via the new
isAjaxproRequest.mark() instead of relying on inference.
Reported on 420shop.nl (customer-facing, all listing pages).
Closes #57 30e663 -
fix: ajaxpro-cart/reinit/product section render must not 400 section/load
Magento\Customer\Controller\Section\Load turns any exception thrown by
a section source into an HTTP 400 for the whole request. Cart::getSectionData()
renders the full cart layout server-side even for an empty/absent quote,
and on some environments (Magento 2.4.9 + Breeze, per report) that render
throws, aborting customer/section/load for every guest with an empty cart.
Wrap the render logic in Cart, Init and Product CustomerData classes in
try/catch: on failure, log via the newly injected Psr\Log\LoggerInterface
and return [] for that section instead of letting the exception bubble up.
Core sections (cart, customer, ...) keep loading either way.
Closes #56 5f061c -
docs: add manual QA scenarios ba69b1
-
-
1.7.36
-
Version 1.7.36 82272a
-
test: config options and type-source coverage
Covers the options x settings surface at the logic level:
- Helper\Config: master switch (every feature off when
ajaxpro/main/enabled is 0), the override-minicart handle whitelist
(allowed only for minicart / shopping-cart handles, matching the
system.xml depends), standalone flags that ignore the master switch,
and type casts.
- Source\Handle / Source\Type: lock the popup handle and cart type
enumerations referenced by name across system.xml, layout and config.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 8f91cf -
test: popup gating matrix across component flavors and popup types
Runs the gating scenarios (popup after add-to-cart, silence on stale
re-fetch, redirect flow) for every combination of component flavor
(luma uiComponent / breeze view) and cartHandle popup type (minicart,
shopping cart, suggest page), plus the simple-popup messages gate and
closeTimeout config variations (disabled, countdown with continue
button, reset on user activity).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ef33d0 -
test: integration coverage of the popup gating chain
Covers section update -> _subscribe -> ModalManager.checkAndShow end
to end: popup after add-to-cart post, popup after redirect-based add
to cart, silence on stale section re-fetch (the bsxl scenario), no
reopen after the popup was shown. This chain is the only protection
on both stacks since disposableCustomerData was dropped (#54).
Refs #54
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 4f7514 -
refactor: drop disposableCustomerData, unify luma and breeze strategy
Luma applied the disposableCustomerData extender to ajaxpro sections
(wiping them from storage and section_data_ids cookie after use) while
Breeze had it commented out — the divergence made stale-section bugs
Breeze-only and the behavior hard to reason about.
Since 048361e the popup opens only within 30s after a real add-to-cart
post (is-ajaxpro-request marker), so a stale re-fetch on page load can
not trigger it on either stack. Ajaxpro sections now persist like any
other customer-data section on both frontends.
Verified on luma storefront: popup on add to cart, section persists,
stale section re-fetch on navigation stays silent.
Closes #54
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> b0a6dd -
refactor: replace PostHelper preference with a before plugin
The preference on Magento\Framework\Data\Helper\PostHelper replaced
the core class entirely and conflicted silently with other modules
customizing the same helper. A before plugin composes instead.
before (not after/around): the decision "caller did not pass uenc
explicitly" is only visible in the original arguments, and no result
re-serialization is needed. Semantics are identical to the old
override: inside an ajax request reuse the real page uenc from the
request parameters.
Old helper class kept as @deprecated for code extending it.
Closes #55
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 4f0ca4 -
chore: strict_types in all php files, drop dead code
- declare(strict_types=1) in every php file (project standard);
verified with php -l, unit tests and storefront smoke run
- remove commented-out debug/dead blocks (md5 debug, unused methods,
disabled section actions)
- drop stale deprecated setup_version from module.xml (no Setup/ dir)
Refs #53
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> a46452 -
test: cover event handling hygiene fixes (issue #52)
Refs #52
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> bf5db7 -
fix: event handling hygiene in frontend js
- Namespace click handlers (.off('click.ajaxpro')) in floating cart
and minicart override: bare .off() also removed handlers attached
by other modules.
- Cap modal-manager register() polling at 10s and register anyway:
on pages with permanent ajax activity $.active never reaches 0 and
the interval lived forever.
- Dispose customer-data subscriptions on destroy (ajaxpro.js,
compare.js) to avoid duplicate handlers after component re-init.
- Fix dead fallback typo: form.prop('prop') -> form.prop('action').
Closes #52
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 635251 -
test: cover modal auto-close timer (issue #51)
Refs #51
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> e53aaf -
fix: modal auto-close timer throws instead of closing the modal
setTimeout received this.closeModal unbound, so the closeTimeout
feature fired with `this` pointing at window and threw a TypeError.
Same defect in luma and breeze variants.
Closes #51
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> d4fe2b -
test: cover ajax notice message suppression (issue #49)
Adds phpunit setup (mirrors module-breeze-theme-editor) and wires
bin/test unit.
Refs #49
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 67257d -
test: cover section reload once-guard (issue #50)
Refs #50
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ceb693 -
fix: infinite section reload loop on website/store mismatch
Minicart and floating cart views reloaded cart sections whenever the
cart section website_id/storeId diverged from window.checkout. When
the quote really belongs to another website/store (same-domain
multi-store), reloading can never fix the mismatch, and since the
Breeze view re-creates on every contentUpdated, the check re-fired
after each reload — an endless cart/ajaxpro-cart request stream.
Reload at most once per page load and guard undefined website_id in
the floating cart (undefined !== websiteId used to trigger it too).
Closes #50
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 84dcbd -
fix: notice messages silently dropped on every ajax request
isMessageToIgnore() compared strpos() result with 1 instead of false,
so it returned true for nearly any message and addNoticeMessage()
discarded all ajax notices — not only the two "choose options"
messages it was meant to suppress.
Closes #49
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 79940e
-
-
head
-
1.0.10
-
head
-
head
-
Speculation rules: fixed not working exclude rules c78263
-
Added missing destructor to pagebuilderCarousel 81d228
-
Do not close dropdownDialog when dragging the slider inside eed793
-
Do not include requirejs-config if it's empty b4fbfe
-
Defer requirejs-config as all other scripts are deferred too 861179
-
Preload image from category-view block if main image is not found de4258
-
-
2.31.4
-
Version 2.31.4 53d648
-
Improve cache key for js block f390bf
-
Update third-party JS libraries
- underscore 1.13.6 -> 1.13.8
- knockout 3.5.1 -> 3.5.3
- js.cookie 3.0.1 -> 3.0.8
- dayjs 1.11.7 -> 1.11.21
- panzoom 4.5.1 -> 4.6.2
- swiped-events 1.1.7 -> 1.2.0
- tabbable 6.1.1 -> 6.5.0
- focus-trap 7.3.1 -> 8.2.2
- scrollyfills 1.0.3 -> 1.0.4
- lite-yt-embed: 0.3.4 1d4f5d -
Use view transition when prefers-reduced-motion is not set to `reduce` c4ed65
-
Remove useless comment 80489b
-
Fixed empty pages in Instagram Browser when view transition is enabled
Closes https://github.com/breezefront/theme-frontend-breeze-evolution/issues/23 1c3432 -
fix(scroll-reveal): guard optional BreezeThemeEditor lookup with module-enabled check
class_exists(BreezeThemeEditor::class) only reflects composer autoload
availability, not whether Swissup_BreezeThemeEditor is actually
enabled. When it's disabled, di.xml preferences aren't loaded, so
ObjectManager::get(BreezeThemeEditor::class) fatals trying to
instantiate the bare ValueRepositoryInterface it depends on
(fixes #100).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> e52077
-
-
head
-
Merge pull request #74 from swissup/fix/issue-73-i18n-dictionary
Regenerate i18n dictionary, fix uncollectable and stale phrases ef2944 -
Regenerate i18n dictionary, fix uncollectable and stale phrases
`en_US.csv` had drifted well behind the source: 106 rows against the 196
phrases `i18n:collect-phrases` finds in the module. Regenerated it and
brought every locale file back in line with it.
Source fixes that had to come first:
- `listing.phtml` built the answer counter label as
`__($collection->getSize() == 1 ? 'answer' : 'answers')`. The phrase
collector cannot resolve a ternary inside `__()`, so it emitted the
literal `answer' 'answers` as a key — which every locale file then
faithfully translated, while the real `answer` and `answers` strings
stayed untranslatable. Moved the ternary outside the `__()` calls.
- `"id" is incorect` -> `"id" is incorrect` (CreateVote.php).
- `MassAssign.php` used `Assign to...` where the UI component label says
`Assign to ...`, producing two dictionary keys for one label. Unified
on `Assign to ...`.
Dictionary changes:
- `en_US.csv` regenerated from `i18n:collect-phrases`, 106 -> 196 rows.
This also drops the stray quoting on `It is helpful` and `Not much
help`, whose values were ` "It is helpful"` and ` "Not much help"` —
rendered on the frontend complete with the quotes.
- Removed 13 dead keys from every locale (`Hint`, `Askit Items`,
`New Askit Question`, `Answer Information`, `You saved.`, the
`${ $.$data.text }` UI-component leftovers, the misspelled
`We can't find a answer to delete.`, and the mangled
`answer' 'answers`).
- `fa_IR.csv`: removed a duplicate `Not much help` row whose value was
double-quoted, and folded the stray ` answer` key into `answer`.
- Normalised the leading space on the `asks` / `about` / `answer` /
`answers` values — the templates already prepend it.
- All files rewritten fully quoted.
Every locale now parses to exactly two columns per row, carries no
duplicate keys, and contains no key absent from `en_US.csv`.
Translating the newly exposed phrases is left to the translators;
coverage per locale is currently 44-49%.
Refs #73
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 0c1b32 -
Merge pull request #72 from swissup/fix/issue-71-locale-csv
Fix malformed "Answer" row in locale files, add cs/sk/hu/ro 8b3f84 -
Fix malformed "Answer" row in locale files, add cs/sk/hu/ro
The "Answer" entry had an unclosed quote in 18 of 19 locale files:
"Answer,Answer
fgetcsv merged that line with the next one, so both "Answer" and
"Entity Type" were lost in every affected locale. Closing the quotes
restores them.
Also:
- add cs_CZ, sk_SK, hu_HU and ro_RO translations
- add the missing "Plain text only. HTML is not allowed.",
"Submit Question" and "Name" keys to en_US, de_DE, it_IT and nl_NL
Translations contributed by @dimdimich in #71. Over-escaped quotes
("""" instead of "") in the submitted hu_HU and ro_RO files were
corrected before merging. fa_IR was already well-formed and is
left untouched.
Fixes #71
Closes #70
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 0381cd
-
-
1.14.25
-
Version 1.14.25 d0112b
-
Merge pull request #69 from swissup/fix/issue-68-null-array-offset
Fix null array offset deprecation on new product form (#68) 1a7529 -
Fix null array offset deprecation on new product form
modifyData() wrote $data[$productId] where $productId is null for a
new (unsaved) product, triggering on PHP 8.5:
Deprecated Functionality: Using null as an array offset is
deprecated, use an empty string instead
Guard against a null product id and return $data unchanged, mirroring
the existing guard in modifyMeta(). current_product_id is meaningless
for a product that does not exist yet, so skipping the write is safe;
editing an existing product is unaffected.
Fixes #68
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> 0fd6d0
-
-
head
-
Remove color from menu links dec081
-
Clone currency and store switchers in header slideout menu for improved functionality de36e3
-
Enhance header and newsletter styles for improved layout and responsiveness in header slideout menu. b63dd0
-
Installer. Update header slideout menu styles. 825795
-
Update footer top content HTML to enhance newsletter section styling.
Don't set font familiy for header in Page Builder ba0b81 -
BCB homepage WIP b228af
-
Basic navpro slideout styles 75ac0e
-
Refactor header slideout menu and newsletter styles for improved layout and consistency 3b42ff
-
Header slideout menu... d4f174
-
Listing styles for the list mode and old price update. cbff61
-
Update README.md to clarify usage of `with-bottom-divider` CSS class and provide detailed instructions 1b9ea5
-
Pagination and make with-bottom-divider general. 37ea17
-
Added list banner 5 config; updated installer, moved it to json a43985
-
Add new layout configuration and remove easybanner references; update footer content and styles 87165b
-
Refactor easybanner configuration and installer for product listing banners 8b1d0d
-
Installer. Listing grid banner added. 914c01
-
Add swatches variables and layout styles d59581
-
Update positioning for filter title and content in layered navigation 8cbd21
-
Add image dimensions for product listings and related widgets in view.xml 6985cd
-
layout and styles for category view, product toolbar, shop by button 08fd27
-
Add layered navigation styles and update imports 944ff1
-
Keep working on prodcut listing and category page. 411088
-
Keep working on product listing toolbar. ebcfac
-
Working on product listing and toolbar look. 1ab39d
-
Add Cooper* font 132906
-
Add listing item styles and variables e725b5
-
Product lisnting in progress - grey bg and border radius. 4e7c35
-
Add new variable files for colors, typography, header, footer, icons, navigation, popover, minisearch, and gradient ba122c
-
Refactor footer and newsletter styles for improved consistency and responsiveness d831e4
-
Footer top newsletter - some updates. 452ca5
-
Add footer top content block and styles for newsletter integration (WIP) 128131
-
Footer bottom. 8d58ea
-
Add footer content block and styles for enhanced footer layout 4d6b1b
-
Header. Hide menu on medium screens. Update search look on monile. e0146d
-
Installer. Add navigation slideout menu a89fd5
-
Add EasySlide functionality for header panel with custom slides and styles e55b0b
-
Add minisearch layout and styles to enhance search functionality 5c5aec
-
Update config.xml and LESS files for theme adjustments and styling improvements of search field 09f88d
-
Add Lexend font and related styles to the theme 0100e8
-
Create config.xml a78b04
-
Create installer.xml 073f18
-
Create view.xml baa3b6
-
Create theme.xml 440a18
-
Create registration.php 119b4f
-
Create README.md 2a5862
-
Create composer.json 16ec29
-
-
head
-
Merge pull request #58 from breezefront/fix/temperature-zero
Stop dropping a temperature of 0 d79542 -
fix(providers): stop dropping a temperature of 0
Claude and Gemini read the value with an assignment inside the condition:
if ($temperature = $config->getExtra('temperature')) {
which is a truthiness test, and `(bool) 0` is false. An admin setting 0 — the
value you pick precisely to make output deterministic — had it silently discarded
and got the provider's own default instead, 1.0 on Claude. No error, no warning,
just different output than asked for.
Compared against null and '' now, so 0 reaches the request and an unset field
still sends nothing. OpenAI was already correct via `?? DEFAULT_TEMPERATURE`.
Found by exercising the admin form rather than by reading: the field only became
settable when the extra_config registry landed, and both existing provider tests
covered temperature at 0.5 and 0.7, never at 0. Tests now cover 0 and unset for
both providers, and fail on the old code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> da9be6 -
Merge pull request #57 from breezefront/refactor/extra-config-registry
One registry for the extra_config fields 40cddc -
Merge pull request #55 from breezefront/feat/vision-image-input
Vision foundation: let a message carry images, encode them per provider fd09a0 -
refactor(config): one registry for the extra_config fields
Which fields live in the extra_config JSON was written down three times: in the
save controller, in the form data provider, and in the form XML. Nothing failed
when they disagreed — a field missing from the save list is silently not stored,
one missing from the read list comes back blank on every edit. Adding `vision`
meant three coordinated edits with no test that would catch a missed one.
`temperature` is what that cost. All three providers read it; neither list
carried it and the form had no field, so it could not be set from the admin at
all. It has a field now, and the notice says what the ranges are and that GPT-5
and the o-series ignore it.
Model/ModelConfig/ExtraFields owns the keys and how each normalises. Types
matter here: a blank submission clears the key, an int at zero clears it — no
token cap means anything at zero — but a float at zero is kept, because zero is
a meaningful temperature and dropping it would silently restore the provider
default for someone asking for deterministic output.
The form XML still declares its own fields: a field needs a label, a notice and a
control that no list can supply. What moved is which keys travel in and out.
Needs setup:di:compile — both consumers gained a constructor argument.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d1dc4b -
refactor(vision): cut ImageBuilder to the shape callers send
Five accepted spellings of the same input for one call site: fromBinary had no
caller at all, and fromArray took `data_uri` beside `url`, `mimeType` beside
`mime_type`, and a data URI hidden in `data`. Each was a guess about a caller
that does not exist — no skill attaches an image yet, and no JS sends one.
What is left is what GenerateService passes: `{mime_type, data}`, or a data URI
under `url` — the key OpenAI's own image_url part uses, so it is the one a caller
already has a name for. fromDataUri and the base64 check became private; the
public surface is fromArray, which is the only method anything calls.
94 lines to 90, and 4 public methods to 1. Adding a shape back is cheap once a
caller wants it; carrying four unused ones is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 66ac12 -
fix(vision): address the review findings on this PR
VisionSupport keeps only the `vision` config key and reads ModelMetadata for
what the name means, so its two prefix tables and its copy of matchesPrefix are
gone — 116 lines to 70. The o-series can no longer be listed here as text-only
while being listed there as reasoning.
ImageValidator collects every image, checks the count, and only then decodes.
The cap was the last rule tested, so an over-cap request paid for N base64
decodes to be refused, and was refused for whichever image happened to be
malformed first rather than for being over cap.
ImageBuilder accepts and discards parameters between the mime type and
`;base64`. A `charset` an exporter felt like adding says nothing about image
bytes, and refusing the URI over it refused a readable image.
GenerateService::toMessage() no longer claims Web API hands history over as
arrays. It does not: /V1/breezeai/generate declares MessageInterface[] and
ServiceInputProcessor dies with "Cannot instantiate interface" before the service
runs, because the interface has no DI preference. The caller that really passes
arrays is the admin controller, which decodes the body itself. History over REST
predates image input and is not fixed here; docs/DEVELOPMENT.md says so instead
of implying the route works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> dd6371 -
fix(data): document every Message constructor parameter
Magento reads @param tags positionally — TypeProcessor::getParamDocBlockTag()
indexes the tag list with the parameter's own offset — so a block documenting
only $images typed $role from the images tag and had no tag to return for
position 2. Reflecting the constructor to build a Message from request data
died there with a TypeError before any of our own validation ran.
The two missing tags are the whole fix. A test pins the invariant, since the
next parameter added to this constructor would silently reintroduce it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> b793a0 -
feat(providers): let a message carry images, and encode them per provider
A message gains getImages(): ImageInterface[] alongside its string content,
and each provider encodes those images into its own wire format — an
Anthropic base64 image block, an OpenAI image_url part holding a data URI,
a Gemini inline_data part. Content stays a string, so a text-only turn
produces the same request body it did before and every existing caller of
`new Message($role, $content)` keeps working.
Two things guard the send. ImageValidator enforces the limits the three
providers share — JPEG/PNG/GIF/WebP, 5 MB decoded per image, 10 per
request, user turns only — and VisionSupport answers whether the model a
call is routed to reads images at all, from the model name plus a `vision`
override on the model row (the new Image Input field). AiRequest runs both
before reaching the provider, so an image sent to a text-only model fails
with one sentence naming the model rather than a provider 400. Callers that
want to know in advance ask AiRequest::supportsVision().
ImageBuilder turns the shapes an image arrives in — raw bytes, a data URI,
a decoded request payload — into an ImageInterface, and GenerateService
uses it so history posted over the wire can carry images too.
No skill attaches an image yet; this is the layer under that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> ad9747 -
Merge pull request #56 from breezefront/refactor/model-metadata
One table for what a model name means eb79b5 -
fix(model): correct the model facts the merge exposed
Putting both axes in one table made three disagreements visible:
o1 reads images. It was absent from the image-input list while present in the
parameter-set one, which is the drift this table exists to stop. o1-mini and
o1-preview genuinely do not, so they say so as longer prefixes.
gpt-4-vision-preview — where vision on GPT-4 started — matched nothing and read
as text-only, because the list carried gpt-4-turbo and gpt-4o but not the model
named for the capability.
claude-4 was dead weight: Claude 4 and later put the tier first, as in
claude-opus-4-5, and those already match claude-opus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> f113ee -
refactor(model): read model facts from one table
RequestBody carried three prefix lists and VisionSupport (in #55) carried two
more, each with its own copy of matchesPrefix. Nothing tied them together, so
they drifted: the parameter-set list knew about o1 and the image-input list did
not, and no test could catch it because neither list knew the other existed.
ModelMetadata holds the facts once, keyed by provider and name prefix. Every
prefix a name starts with contributes, shortest first, so a longer prefix
overrides a shorter one and states only what differs — o3-mini says it takes no
image and inherits its parameter set from o3. That ordering is what removes the
separate lists of exceptions each consumer used to keep beside its main list.
A per-row sampling or vision value stays with the consumer that reads it: the
row overriding the name is config, not a fact about the model.
Behaviour is unchanged — every existing RequestBody test passes untouched, and
the table reproduces the old lists name for name, o1's missing image support
included. Correcting that is the next commit, kept separate so this one reads
as the move it is.
supportsVision() has no caller on master yet; #55 is the caller and rebases
onto this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 891642 -
feat(providers): let a message carry images, and encode them per provider
A message gains getImages(): ImageInterface[] alongside its string content,
and each provider encodes those images into its own wire format — an
Anthropic base64 image block, an OpenAI image_url part holding a data URI,
a Gemini inline_data part. Content stays a string, so a text-only turn
produces the same request body it did before and every existing caller of
`new Message($role, $content)` keeps working.
Two things guard the send. ImageValidator enforces the limits the three
providers share — JPEG/PNG/GIF/WebP, 5 MB decoded per image, 10 per
request, user turns only — and VisionSupport answers whether the model a
call is routed to reads images at all, from the model name plus a `vision`
override on the model row (the new Image Input field). AiRequest runs both
before reaching the provider, so an image sent to a text-only model fails
with one sentence naming the model rather than a provider 400. Callers that
want to know in advance ask AiRequest::supportsVision().
ImageBuilder turns the shapes an image arrives in — raw bytes, a data URI,
a decoded request payload — into an ImageInterface, and GenerateService
uses it so history posted over the wire can carry images too.
No skill attaches an image yet; this is the layer under that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 659e74
-
-
1.1.0
-
Version 1.1.0 9aeb4e
-
Merge pull request #54 from breezefront/feat/bulk-run-model-and-system-prompt
feat(bulk): pick the model and the framing per run, and pin them to it cab8ed -
fix(bulk): keep the closing script tag out of a JS comment
The comment warning about a prompt body holding a closing script tag
contained one, which ended the block early and dumped the rest of the
script onto the page as text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d1f118 -
feat(bulk): pick the model and the framing per run, and pin them to it
The bulk wizard ran on whatever the configuration said at the moment each
operation was picked up, with no way to steer the model or tell it what
kind of writing was wanted. The client rated the German output 4/10 and
asked for both: a model per run, and a free-text instruction that reaches
the model as framing rather than as part of the product text.
Two things had to be true for that to mean anything.
The framing has to arrive in the system channel, ahead of the content.
It does: the skills put it in a system message, and the providers already
agree that a caller's system message wins over their built-in default.
Where a run brief is present the translate prompt drops its own "you are
a professional e-commerce translator" opening — that line answers the one
question the brief exists to answer — while keeping the mechanical rules
about numbers, SKUs and HTML, which are output format rather than
persona.
And both have to hold still. A batch of a few thousand products drains
over hours, so resolving configuration per operation means editing the
default model halfway through silently splits the batch across two
models with nothing in the result to say which got which. The model and
the prompt are now decided when the run is scheduled and travel in the
operation payload, which is already immutable once written — no queue
schema change. Even leaving the dropdown alone pins a concrete row:
"the default model" is a moving target.
The model ID is a pointer and the row behind it stays editable, so the
provider and model name ride along as a snapshot and the consumer stops
rather than bill a model nobody chose. The API key is deliberately not
snapshotted — rotating one has to reach the runs already queued.
For Generate this reverses an existing precedence: a model passed in now
beats the one bound to the prompt. Step 5 tells the operator which model
the run will use, and a per-prompt model overriding that would make the
screen lie. Nothing outside the wizard passes one, so the prompt's own
model still decides everywhere else.
Two small stores back the UI. Prompts gain a type, so a run brief can be
saved in the existing library without turning up in the per-field
dropdowns; rows written before the column are field prompts, which is all
the module could store then. And one row per admin remembers what the
wizard was last run with, so it opens on that instead of an empty form —
a preference, not a reference, which is why it carries no foreign key to
the model table: a model since deleted is simply not offered.
Operations queued before any of this carry none of the new keys and keep
running on the default model.
setup:upgrade applies the schema; Claude, Gemini and the bulk consumer
take new constructor arguments, so setup:di:compile is needed as well.
./bin/test unit — OK (454 tests, 901 assertions)
phpcs --standard=Magento2 — 0 errors on every touched file
Schema verified against a real database, not only declared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> fcb9d8 -
fix(providers): agree on what a caller's system message means (#53)
* fix(providers): agree on what a caller's system message means
The three providers disagreed. OpenAI lets a caller-supplied system message
replace the built-in framing; Claude and Gemini filtered it out of the
conversation and then sent their own constant anyway, so the instruction was
read, dropped, and never reached the model. The same prompt therefore meant
different things depending on which model it happened to be assigned to — and
silently, because nothing errors when a system message is discarded.
Nothing sends one today, so this fixes no live failure. It removes a trap: the
first skill that reaches for its own framing would have got it on OpenAI and
lost it on the other two, and the symptom is bad output rather than an error.
`Model\Provider\SystemMessage` now resolves the framing for all three — the
caller's, or the built-in default when the caller supplied none. Where it goes
still differs per API: inline in `messages` for OpenAI, the top-level `system`
field for Claude, `system_instruction` for Gemini. More than one system message
is joined in order rather than reduced to the first, since Claude and Gemini
have a single field for it and dropping the rest would lose instructions.
The default string lived in three copies; `RequestBody::SYSTEM_MESSAGE` now
points at the shared constant so it keeps working for anything referencing it.
Claude and Gemini take a new constructor argument, so this needs
`setup:di:compile`.
./bin/test unit — OK (409 tests, 810 assertions)
phpcs --standard=Magento2 — 0 errors on all touched files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(openai): resolve the framing through the shared rule, not a copy of it
The previous commit gave Claude and Gemini a shared SystemMessage but left
OpenAI deciding for itself in buildMessages(). Only the default string was
shared, so the two paths could still drift: several system messages were
joined on Claude and Gemini and left scattered through the conversation on
OpenAI, and any later change to resolve() would have reached two providers
out of three.
OpenAI now calls resolve() as well. The framing still travels as the first
entry of `messages` because that is what the endpoint takes; the caller's
system messages are lifted out of the conversation and folded into it, so
the instruction is sent once rather than twice.
resolve() also stops treating a blank system message as one supplied. An
empty string is not framing, and passing it through meant Gemini receiving
an empty `system_instruction` part — which it refuses — where before the
constant always went out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> 2c716b -
fix(openai): resolve the framing through the shared rule, not a copy of it
The previous commit gave Claude and Gemini a shared SystemMessage but left
OpenAI deciding for itself in buildMessages(). Only the default string was
shared, so the two paths could still drift: several system messages were
joined on Claude and Gemini and left scattered through the conversation on
OpenAI, and any later change to resolve() would have reached two providers
out of three.
OpenAI now calls resolve() as well. The framing still travels as the first
entry of `messages` because that is what the endpoint takes; the caller's
system messages are lifted out of the conversation and folded into it, so
the instruction is sent once rather than twice.
resolve() also stops treating a blank system message as one supplied. An
empty string is not framing, and passing it through meant Gemini receiving
an empty `system_instruction` part — which it refuses — where before the
constant always went out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7ddc5f -
fix(providers): agree on what a caller's system message means
The three providers disagreed. OpenAI lets a caller-supplied system message
replace the built-in framing; Claude and Gemini filtered it out of the
conversation and then sent their own constant anyway, so the instruction was
read, dropped, and never reached the model. The same prompt therefore meant
different things depending on which model it happened to be assigned to — and
silently, because nothing errors when a system message is discarded.
Nothing sends one today, so this fixes no live failure. It removes a trap: the
first skill that reaches for its own framing would have got it on OpenAI and
lost it on the other two, and the symptom is bad output rather than an error.
`Model\Provider\SystemMessage` now resolves the framing for all three — the
caller's, or the built-in default when the caller supplied none. Where it goes
still differs per API: inline in `messages` for OpenAI, the top-level `system`
field for Claude, `system_instruction` for Gemini. More than one system message
is joined in order rather than reduced to the first, since Claude and Gemini
have a single field for it and dropping the rest would lose instructions.
The default string lived in three copies; `RequestBody::SYSTEM_MESSAGE` now
points at the shared constant so it keeps working for anything referencing it.
Claude and Gemini take a new constructor argument, so this needs
`setup:di:compile`.
./bin/test unit — OK (409 tests, 810 assertions)
phpcs --standard=Magento2 — 0 errors on all touched files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 435eb3 -
fix(openai): send the parameter set the model family accepts (#52)
* fix(openai): send the parameter set the model family accepts
Every request carried temperature, frequency_penalty and presence_penalty,
and max_tokens when configured. That is the GPT-4 parameter set. The GPT-5
family and the o-series refuse all four on the same endpoint: the two
penalties are rejected outright even at 0, temperature takes nothing but
its default, and the token cap was renamed to max_completion_tokens. A
request built the old way comes back 400, so those models did not degrade
on this provider — they failed every call.
Nobody hit it because the model dropdown stopped at gpt-4o-mini and
reaching a newer one meant typing it into the custom field. The client on
the translation project asked for gpt-5.6-terra, which is where this
starts to matter.
The body is now built by a small class of its own. The provider builds its
HTTP client inline and cannot be exercised without a network, which left
the part that has to be right per model family with no test at all; that
part is now pure and covered.
Family is decided by the model name, with a sampling extra-config key to
override it: an Azure deployment or a proxy can serve GPT-5 under a name
that says nothing, and prefix matching would send it what it refuses.
reasoning_effort and verbosity are passed through where they are
understood, so the effort knob is reachable without another release.
A caller-supplied system message now replaces the built-in one instead of
being appended after it — two system messages in one request left the
model to pick. No caller sends one yet.
The three GPT-5.6 tiers are in the model dropdown, so the client's model
is a selection rather than a hand-typed string.
phpcs --standard=Magento2 reports 0 errors on the touched files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(openai): gate the reasoning knobs per family and expose them in admin
The first pass treated GPT-5 and the o-series as one family. They are not:
`verbosity` shipped with GPT-5 and the o-series rejects it, and `o1-mini`
rejects `reasoning_effort` as well. Sending either to a model that refuses it
reproduces the 400 this branch set out to remove, so the knobs are now offered
per family while the token-cap rename and the dropped sampling parameters stay
common to both.
The knobs themselves had no way in. `sampling`, `reasoning_effort` and
`verbosity` were readable only by hand-editing the extra_config JSON, which
made the escape hatch for unnamed Azure and proxy deployments unreachable in
practice. All three are now fields on the model form, saved into and read back
out of extra_config alongside max_tokens.
Also:
- honour a configured `temperature` on the GPT-4 path instead of pinning 1.0,
matching what the Claude and Gemini providers already do
- read `sampling` without casting, so a non-string value in the JSON cannot
raise an array-to-string conversion
- note in the class docblock that an unrecognised model name falls back to the
GPT-4 parameter set, so the next family will need its prefix added here
./bin/test unit — OK (401 tests, 802 assertions)
phpcs --standard=Magento2 — 0 errors on all touched files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> 37217f -
fix(openai): gate the reasoning knobs per family and expose them in admin
The first pass treated GPT-5 and the o-series as one family. They are not:
`verbosity` shipped with GPT-5 and the o-series rejects it, and `o1-mini`
rejects `reasoning_effort` as well. Sending either to a model that refuses it
reproduces the 400 this branch set out to remove, so the knobs are now offered
per family while the token-cap rename and the dropped sampling parameters stay
common to both.
The knobs themselves had no way in. `sampling`, `reasoning_effort` and
`verbosity` were readable only by hand-editing the extra_config JSON, which
made the escape hatch for unnamed Azure and proxy deployments unreachable in
practice. All three are now fields on the model form, saved into and read back
out of extra_config alongside max_tokens.
Also:
- honour a configured `temperature` on the GPT-4 path instead of pinning 1.0,
matching what the Claude and Gemini providers already do
- read `sampling` without casting, so a non-string value in the JSON cannot
raise an array-to-string conversion
- note in the class docblock that an unrecognised model name falls back to the
GPT-4 parameter set, so the next family will need its prefix added here
./bin/test unit — OK (401 tests, 802 assertions)
phpcs --standard=Magento2 — 0 errors on all touched files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7d1b03 -
fix(openai): send the parameter set the model family accepts
Every request carried temperature, frequency_penalty and presence_penalty,
and max_tokens when configured. That is the GPT-4 parameter set. The GPT-5
family and the o-series refuse all four on the same endpoint: the two
penalties are rejected outright even at 0, temperature takes nothing but
its default, and the token cap was renamed to max_completion_tokens. A
request built the old way comes back 400, so those models did not degrade
on this provider — they failed every call.
Nobody hit it because the model dropdown stopped at gpt-4o-mini and
reaching a newer one meant typing it into the custom field. The client on
the translation project asked for gpt-5.6-terra, which is where this
starts to matter.
The body is now built by a small class of its own. The provider builds its
HTTP client inline and cannot be exercised without a network, which left
the part that has to be right per model family with no test at all; that
part is now pure and covered.
Family is decided by the model name, with a sampling extra-config key to
override it: an Azure deployment or a proxy can serve GPT-5 under a name
that says nothing, and prefix matching would send it what it refuses.
reasoning_effort and verbosity are passed through where they are
understood, so the effort knob is reachable without another release.
A caller-supplied system message now replaces the built-in one instead of
being appended after it — two system messages in one request left the
model to pick. No caller sends one yet.
The three GPT-5.6 tiers are in the model dropdown, so the client's model
is a selection rather than a hand-typed string.
phpcs --standard=Magento2 reports 0 errors on the touched files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3b7370 -
Merge pull request #51 from breezefront/fix/category-translate-source-language
fix(translate): name the source language when translating categories db336b -
docs(translate): tighten the source-store comment
The docblock read like a changelog entry for 1.0.11. Keep the reason the
source store is handed to the skill, drop the history around it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> f63c1b -
fix(translate): name the source language when translating categories
1.0.11 changed the translate prompt to name both languages ("translate
from Danish to German") because a weak model, told only the target, left
a good part of the text in the source language. The bulk consumer was
only half-converted: translateProducts() passes source_store_id, but
translateCategories() still calls the skill with the attribute and the
target store alone.
The source store was already used to load the category, so the values fed
to the model were right — only the prompt was missing the "from" half.
Category translations therefore kept running on the pre-1.0.11 prompt and
came back partly untranslated, the exact symptom 1.0.11 was released to
cure. Nothing surfaced it: the skill treats source_store_id as optional
and silently omits the clause when it is absent.
The unit test now asserts the whole options array rather than accepting
any call, so the next field added to it cannot go missing the same way.
phpcs --standard=Magento2 reports 0 errors on the touched files, the same
as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 6871f1 -
Merge pull request #50 from breezefront/fix/bulk-empty-field-panels-v2
fix(bulk): do not offer a task whose field panel has nothing in it acd541 -
fix(bulk): do not offer a task whose field panel has nothing in it
The wizard decides what step 2 may offer by asking whether a step 4 panel
exists for the pair. A panel could exist and still be empty: Generate
renders a row per attribute only when a prompt is bound to it, so with no
prompts configured the panel is a bare fieldset. The check said yes, the
operator picked Generate, and step 4 came up blank with a Run button that
never enabled — the dead end the panel check was meant to close, reached
from inside the panel instead of around it.
Prompts are now resolved once up front and an attribute without any is
dropped, which makes "the panel has fields" knowable before rendering.
A panel with no fields is not rendered, so the existing check reports the
truth for it. Translate gets the same treatment for symmetry, though its
field list is only empty on entity types nobody can select today.
That leaves step 2 able to come up with nothing at all in it, so it now
says why instead of showing an empty box.
phpcs --standard=Magento2 reports 0 errors on the touched file, the same
as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 392330 -
Merge pull request #48 from breezefront/fix/bulk-save-skill-entity-validation
fix(bulk): refuse a skill the queue cannot run on the chosen entity type 24f2e1 -
Merge pull request #49 from breezefront/fix/bulk-empty-field-panels
fix(bulk): do not offer a task whose field panel has nothing in it ecfa84 -
Merge pull request #46 from breezefront/fix/bulk-wizard-skill-filter
fix(bulk): decide the offered tasks by the step 4 panel, not the skill's word 87738f -
fix(bulk): refuse a skill the queue cannot run on the chosen entity type
BulkConsumer::execute() routes on (skill, entity type) pairs and throws on
anything it has no handler for, but the scheduling controller accepted
whatever was posted. A request for a pair the consumer cannot route —
generate on cms_page, say, which the wizard hides but the form will still
carry — was answered with "has been scheduled", split into operations, and
failed one by one where only the bulk log would show it.
Save now checks the pair before it schedules anything, so the operator is
told no while still looking at the screen. The check runs before the entity
ids are resolved, which also spares a full-catalog id query for a request
that was never going to run.
QueueRoutes holds the pairs rather than inlining them, because the
consumer's routing table is the real authority and the two must not drift:
QueueRoutesTest reads the consumer's match arms and asserts they are
exactly the declared list.
It is deliberately not SkillSupport. That asks whether a skill can act on
an entity type at all and answers from getSupportedEntityTypes(); Generate
claims cms_page for the inline editor and is right to, but there is no
generate/cms_page route in the consumer. What a skill can do and what the
queue can carry out are separate facts, and only the second one decides
what may be scheduled.
370 unit tests pass. phpcs --standard=Magento2 reports 0 errors on the
touched files, and Save.php carries the same 10 warnings as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 600476 -
Merge pull request #47 from breezefront/fix/cms-page-translate-scope
fix(translate): stop writing CMS page translations into a category 219f60 -
fix(bulk): do not offer a task whose field panel has nothing in it
The wizard decides what step 2 may offer by asking whether a step 4 panel
exists for the pair. A panel could exist and still be empty: Generate
renders a row per attribute only when a prompt is bound to it, so with no
prompts configured the panel is a bare fieldset. The check said yes, the
operator picked Generate, and step 4 came up blank with a Run button that
never enabled — the dead end the panel check was meant to close, reached
from inside the panel instead of around it.
Prompts are now resolved once up front and an attribute without any is
dropped, which makes "the panel has fields" knowable before rendering.
A panel with no fields is not rendered, so the existing check reports the
truth for it. Translate gets the same treatment for symmetry, though its
field list is only empty on entity types nobody can select today.
That leaves step 2 able to come up with nothing at all in it, so it now
says why instead of showing an empty box.
phpcs --standard=Magento2 reports 0 errors on the touched file, the same
as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 1fe13b -
fix(translate): refuse an unsupported entity type in the skill itself
Narrowing getSupportedEntityTypes() to product and category told the truth
but gated nothing. Three paths still reached a CMS page:
- McpServer::executeSkill() passes entityType straight to GenerateService;
the schema enum is a hint to the client, not a check. translate +
cms_page + targetStore>0 went through Translate::execute(), which took
the attribute list from the input field pool — and the pool still knows
the cms_page fields — translated the text, saved nothing, and reported
success. A silent no-op instead of an error.
- The CMS page form kept its "AI Translate" buttons. Block\Adminhtml\Js
hands translate.js every input field of the entity type, and the form
has no store switcher, so currentStore is always 0: every click ran
translateForAllStores() and died on the new save guard.
- That death cost one AI request. translateForAllStores() calls the skill
first and saves after, so the provider was paid before anything refused
the entity.
Translate::execute() now asserts the entity type against its own
getSupportedEntityTypes() right after resolving it — before the provider
call, so a refused entity costs nothing, and for every caller at once.
saveAttributeForStore() keeps its throw as the last line of defence.
SkillSupport answers "can this skill act on this entity type" for UI code;
Js publishes skills.translate and translate.js renders no buttons when it
is false. Generate still gets the full field list.
Js gained a constructor argument — setup:di:compile.
364 unit tests pass. phpcs --standard=Magento2 reports no errors on the
touched files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 551992 -
refactor(bulk): give the step 4 panel lookup one home
The panel query behind the wizard's "can this task run here" check was
written out three times over. wizardCanRun() and hasFieldSelection() ran
byte-identical selectors, each with its own empty-argument guard, so a
change to how a panel is addressed had to be made in more than one place
to hold.
findPanel() is now that one place, and both callers read as what they
ask: does a panel exist, and does the panel that exists have a selection.
The Build Landing Page panel also rendered unconditionally, though step 1
offers CMS pages only when the Content Builder is installed. Without it
the panel was unreachable markup in every admin; it now follows the same
condition as the content type it serves.
No behaviour change. 358 unit tests pass; phpcs --standard=Magento2
reports the same 0 errors as before on the touched file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 6d13ac -
fix(translate): stop writing CMS page translations into a category
translateForAllStores() routes every non-product entity through
saveAttributeForStore()'s else branch, which loads a *category* by the entity's
id. For a CMS page that id is a page_id, so translating page 8 for all store
views wrote its translated meta_title onto category 8 — on this install, "New
Luma Yoga Collection". A silent write into an unrelated record, no error, no
trace.
Extending the branch is not the fix either: cms_page has nowhere to put a
per-store-view value. The table holds one row per page — title, meta and content
are not store-scoped — and the store assignment lives in cms_page_store. A
translated page in Magento is a separate page. So Translate stops claiming
cms_page, which also narrows the MCP tool's entityType enum, and
saveAttributeForStore() raises on anything it does not explicitly handle instead
of guessing: entity ids collide freely across tables, and guessing corrupts.
Generate keeps cms_page — that path is real. GenerateService::loadEntity()
already resolves pages through PageRepository, so the AI button works on the SEO
fields of the CMS page form. content_heading joins them: plain text, same
mechanics as product-name, and the one obvious gap. title and identifier are
left out on purpose (identifier drags url rewrites behind it), and content
belongs to the Build Landing Page skill, which writes the whole document.
Bulk is unaffected: BulkConsumer never handled generate/cms_page and still does
not, so the wizard offers Build Landing Page alone for CMS pages.
Verified against the compiled DI:
cms_page fields content_heading, meta_title, meta_keywords, meta_description
generate [product, category, cms_page]
translate [product, category]
build_page [cms_page]
360 unit tests pass, including one that pins the corruption: translating a CMS
page for all store views now raises and never touches the category repository.
phpcs --standard=Magento2 reports no errors on the touched files. afe08f -
fix(bulk): decide the offered tasks by the step 4 panel, not the skill's word
Picking CMS Pages still listed Generate Content and Translate: both declare
cms_page in getSupportedEntityTypes() for the inline editor, so the filter added
in the previous commit let them through. Choosing either landed on an empty step
4 with no step 5 and no Run button — the exact dead end this branch set out to
remove, just reached by another route.
A skill's own entity list says nothing about whether this screen can drive it.
The wizard now asks the only thing that matters: does a step 4 panel exist for
this (content type, task) pair. That is the condition for not dead-ending, and it
stays true as panels are added or removed.
getSkillEntityTypes() existed only to feed the old check and is dropped with it.
Verified in the admin: CMS Pages now offers Build Landing Page alone, step 3 is
skipped, step 4 reads "Describe the page", step 5 lists the 84 pages and Run
schedules "AI Build Landing Page for 1 CMS page". 92afa6 -
fix(bulk): decide the offered tasks by the step 4 panel, not the skill's word
Picking CMS Pages still listed Generate Content and Translate: both declare
cms_page in getSupportedEntityTypes() for the inline editor, so the filter added
in the previous commit let them through. Choosing either landed on an empty step
4 with no step 5 and no Run button — the exact dead end this branch set out to
remove, just reached by another route.
A skill's own entity list says nothing about whether this screen can drive it.
The wizard now asks the only thing that matters: does a step 4 panel exist for
this (content type, task) pair. That is the condition for not dead-ending, and it
stays true as panels are added or removed.
getSkillEntityTypes() existed only to feed the old check and is dropped with it.
Verified in the admin: CMS Pages now offers Build Landing Page alone, step 3 is
skipped, step 4 reads "Describe the page", step 5 lists the 84 pages and Run
schedules "AI Build Landing Page for 1 CMS page". 7413c9 -
Merge pull request #45 from breezefront/feat/bulk-cms-landing-page
feat(bulk): build landing pages from the AI Bulk Action wizard eeabe7 -
fix(bulk): file the generated page draft under the scheduling admin
BCB drafts are per user: Content/Load reads getDraft($pageId, 0, $currentUserId)
and the collection filters on equality, so a draft written for anyone else is
invisible in the editor. bcb_page_content.user_id is also a foreign key on
admin_user, so the placeholder 0 the consumer was passing failed the insert
outright — every build_page operation died with an integrity violation and no
draft was ever stored. The unit tests missed it because DraftWriter is mocked.
The scheduling admin's id now travels in the payload and DraftWriter requires it,
rejecting 0 before touching the repository. Save refuses to schedule a page build
when the current user cannot be identified, rather than queueing work that cannot
land.
Alongside that:
- CMS Pages is only offered when the Content Builder is installed. Without it
nothing can store the document, so the wizard used to run the whole batch
through the AI only to fail on save. Step 1, the grids and the LISTING map are
all derived from ViewData::getEntityTypes() now.
- Step 4's heading follows the picked task: "Describe the page" for Build Landing
Page instead of the field-list copy, which still said "Select attributes such
as name, description, meta title".
- Operator messages use real names: "AI Build Landing Page for 3 CMS pages"
rather than "AI Build_page for 3 cms_page(s)".
- Step 4 warns that the run replaces the current draft on every selected page.
- Dropped the unused is_active column from the CMS page grid's select.
Tests: DraftWriterTest covers both guards and the write, SaveTest covers the
payload and the messages, BulkConsumerTest asserts the user id reaches the
writer and that a payload without one fails the operation. 359 pass; phpcs
--standard=Magento2 reports no errors. e8f294 -
feat(bulk): build landing pages from the AI Bulk Action wizard
Step 2 listed every registered skill, so Build Landing Page was offered for
products and categories. It declares cms_page and no attributes, leaving step 4
empty and step 5 unreachable — the wizard dead-ended with no error.
Tasks are now filtered by getSupportedEntityTypes() against the content type
picked in step 1, and CMS Pages joins that step so the skill has a real path:
a brief instead of the per-attribute field list, a CMS page grid, and no store
view step (Content Builder documents are store-agnostic).
The generated document is stored as the page's Content Builder draft. The BCB
page flag is left alone: turning it on with nothing published renders a blank
storefront page, so enabling the editor and publishing stay manual. The BCB
dependency stays soft — the repository is resolved by name behind
interface_exists(), like the component catalog reader.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> afe7f0
-
-
head
-
1.19.2
-
v1.8.2
-
Version v1.8.2 845f50
-
Merge pull request #23 from swissup/fix/topmenu-cms-links-navigationpro
feat(account): CMS links in the My Account dropdown, plus Navigation Pro fix for the top menu 5adad3 -
feat(account): let the client add links to the My Account dropdown
Nabi's screenshot turned out to be the My Account dropdown, not the category
menu — so the block added in #22 was aimed at the wrong place.
That dropdown is the top.links block. Magento\Customer\Block\Account\Navigation
appends child blocks that do not implement SortLinkInterface after the sorted
links, inside its <ul class="header links">, so a Cms\Block\Block there renders
plain <li> items right where the extra links belong.
Ships through the installer like topmenu_cms_links, with a commented example as
its default content.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3d5817 -
fix(menu): render the CMS links when Navigation Pro drives the top menu
On the dev store the links never appeared: navigationpro/top/identifier is
set per store view, and Swissup\Navigationpro\Block\Menu::_prepareLayout()
calls unsetElement('catalog.topnav') in that case — taking our child block
with it.
Navigation Pro's own menu.phtml prints getChildHtml() inside its <ul> too,
so declaring the block under navpro.topnav as well covers both setups. Only
one of the two ever renders, since catalog.topnav is gone whenever navpro is
active, so there is no risk of duplicate links.
Also add li-item to the recommended <li> classes. Navigation Pro's top-level
items carry it (li-item level0 ... level-top) and its CSS resets key off it;
for the stock menu it is a harmless extra class.
Verified both paths on the sandbox with the same store view, toggling
navigationpro/top/identifier on and off.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 12f808
-
-
v1.8.1
-
Version v1.8.1 8845af
-
Merge pull request #22 from swissup/feat/topmenu-cms-links
feat(menu): let the client add top menu links via CMS block 3d0f83 -
feat(menu): let the client add top menu links via CMS block
Nabi asked how to add a link to the top menu without touching the theme
repository. The menu is built from the category tree, so there was no
admin-editable place for extra links.
Magento_Theme::html/topmenu.phtml calls getChildHtml() after the category
tree, so a child block of catalog.topnav lands inside the menu <ul>. A
Cms\Block\Block there renders its content with no wrapper, which makes a
plain <li> a proper sibling of the category items.
The block is shipped through the marketplace installer, so it exists in the
admin right after deploy. Its default content is a commented example only,
so nothing appears in the menu until links are actually added.
Run once per store view — the installer filters by store, so the same
identifier gives an independently editable block per language:
bin/magento marketplace:package:install swissup/argento-420shop \
cms-block=topmenu_cms_links --store=<id>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 66c936 -
Merge pull request #21 from swissup/fix/quickview-qty-clipping
fix(qty): stop quick view clipping quantities of 10 or more 541862 -
fix(qty): stop quick view clipping quantities of 10 or more
The qty switcher styles were nested inside `.catalog-product-view`, but the
Ajaxpro quick view renders the same product markup on category and search
pages, where that body class is absent. In the popup the input therefore fell
back to breeze's `max-width: 4rem` (64px) while keeping the legacy
`padding: 5px 34px 5px 15px` — leaving an 11px content box. At 18px font that
is one digit, so every value >= 10 rendered as its first digit only.
Scope both the input sizing and the switcher arrows to
`.product-info-wrapper` instead, and drop the 34px right padding: it reserved
room for an in-input arrow design that no longer exists — the arrows are
absolutely positioned outside the field. Verified on the live dev store:
content box goes 11px -> 64px, `scrollWidth` no longer exceeds `clientWidth`
for 10, 20 or 100.
The padding rule is now specific enough (0,4,0) to win over the stale
`.product-info-main .input-text.qty { padding: 5px 34px 5px 15px }` still
present in the theme editor CSS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 439222
-