From 1e4ad093accfca7ac1bc0119fcbe44d250b67c35 Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:43:06 +0100 Subject: [PATCH 1/5] chore: fix Prettier formatting in ai-tools test and toolbar CSS Co-Authored-By: Claude Opus 4.6 --- packages/toolbar/__tests__/ai-tools.test.ts | 11 +++++++++-- packages/toolbar/src/toolbar.css | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/toolbar/__tests__/ai-tools.test.ts b/packages/toolbar/__tests__/ai-tools.test.ts index 104d293..2006235 100644 --- a/packages/toolbar/__tests__/ai-tools.test.ts +++ b/packages/toolbar/__tests__/ai-tools.test.ts @@ -380,7 +380,11 @@ describe('getTools', () => { type: 'ais.autocomplete', container: '#search', placement: 'before', - parameters: { container: '#other', placement: 'after', showRecent: true }, + parameters: { + container: '#other', + placement: 'after', + showRecent: true, + }, }, { toolCallId: 'tc1', messages: [] } ); @@ -656,7 +660,10 @@ describe('getTools', () => { expect(result).toMatchObject({ success: true, - applied: ['cssVariables.primary-color-rgb', 'cssVariables.secondary-color'], + applied: [ + 'cssVariables.primary-color-rgb', + 'cssVariables.secondary-color', + ], }); expect(callbacks.onCssVariableChange).toHaveBeenCalledTimes(2); }); diff --git a/packages/toolbar/src/toolbar.css b/packages/toolbar/src/toolbar.css index a3d36f2..5408d7e 100644 --- a/packages/toolbar/src/toolbar.css +++ b/packages/toolbar/src/toolbar.css @@ -127,7 +127,8 @@ background: none; padding: 0; } - ul, ol { + ul, + ol { margin: 0.25em 0; padding-left: 1.25em; } From 7ab4321e603523cc2926578e086eda5fa9004ef5 Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:04:17 +0100 Subject: [PATCH 2/5] feat: add ais.searchBox widget support to runtime and toolbar Register the searchBox widget in the runtime with a passthrough transformParams (bucket 1 only). Enable it in the toolbar with full parameter editing: placeholder, autofocus, searchAsYouType, ignoreCompositionEvents, show flags, and cssClasses. Add a Tooltip UI component and info tooltips on field labels to surface paramDescriptions in the toolbar UI. Add an /add-widget skill to document the repeatable process for onboarding new widgets. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/add-widget/SKILL.md | 30 ++++---- packages/runtime/src/experiences/types.ts | 7 +- packages/runtime/src/experiences/widget.tsx | 9 +++ packages/toolbar/src/widget-types.tsx | 79 ++++++++++++++++++++- 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/.claude/skills/add-widget/SKILL.md b/.claude/skills/add-widget/SKILL.md index 9ff0c20..1cebf15 100644 --- a/.claude/skills/add-widget/SKILL.md +++ b/.claude/skills/add-widget/SKILL.md @@ -4,28 +4,28 @@ Add support for the InstantSearch.js widget `$ARGUMENTS`. ## Reference -Use the [widget parameter buckets research](https://algolia.atlassian.net/wiki/x/CwBLkgE) to determine which parameters belong to each bucket (serializable, templates, functions) for this widget. +Use the [widget parameter buckets research](https://algolia.atlassian.net/wiki/spaces/AE/pages/6749356043/InstantSearch.js+widget+parameters+Bucket+Categorization) to determine which parameters belong to each bucket (serializable, templates, functions) for this widget. ## Steps ### 1. Register the widget in `packages/runtime/src/experiences/widget.tsx` -- Import the widget factory from `instantsearch.js/es/widgets//` (check `node_modules/instantsearch.js/es/widgets/` for the exact path. The directory name often uses hyphens, while the widget name uses camelCase, e.g., `searchBox` → `search-box`). +- Import the widget factory from `instantsearch.js/es/widgets//` (check `node_modules/instantsearch.js/es/widgets/` for the exact path — the directory name often uses hyphens where the widget name uses camelCase, e.g., `searchBox` → `search-box`). - Add the widget key to `$$supportedWidgets` inside the `experience` function, right before the closing `},` of the object. -- For bucket-1-only widgets (all parameters are serializable), `transformParams` is a passthrough: +- For bucket-1-only widgets (all params are serializable), `transformParams` is a passthrough: ```ts 'ais.': { widget: , - async transformParams(parameters) { - return parameters; + async transformParams(params) { + return params; }, }, ``` -- For widgets with bucket 2/3 parameters that need decomposition, model `transformParams` after the existing `ais.chat` or `ais.autocomplete` entries. +- For widgets with bucket 2/3 params that need decomposition, model `transformParams` after the existing `ais.chat` or `ais.autocomplete` entries. - Add TODO comments above the entry listing unsupported parameters by bucket: ```ts // TODO: Add support for `templates` () - // TODO: Add support for `` (bucket 3 function) + // TODO: Add support for `` (bucket 3 — function) ``` Only add TODOs for buckets that have parameters for this widget. Skip if the widget is bucket 1 only. @@ -43,19 +43,19 @@ Use the [widget parameter buckets research](https://algolia.atlassian.net/wiki/x The widget likely already exists in `WIDGET_TYPES` with `enabled: false`. Update it: - Set `enabled: true`. -- Add a `description` string (one sentence describing what the widget does, used by the AI assistant). -- Set `defaultParameters` to include `container: ''` plus all bucket 1 parameters with sensible defaults (empty string for text, `false` for booleans that are off by default, `true` for booleans that are on by default). **Always verify exact parameter names and defaults from the widget's TypeScript types** in `node_modules/instantsearch.js/es/widgets/` or `node_modules/instantsearch.js/es/connectors/`, do not guess from memory (e.g., it's `showSubmit`/`showReset`, not `showSubmitButton`/`showResetButton`). -- Add `fieldOrder` array listing: `'container'`, `'placement'`, then all other parameters in logical order. -- Add `fieldOverrides` for non-string parameters: - - Boolean parameters: `{ type: 'switch', label: '' }` - - Object parameters with enable/disable + sub-fields: `{ type: 'object', label: '...', defaultValue: {...}, fields: [...] }` +- Add a `description` string (one sentence describing what the widget does — used by the AI assistant). +- Set `defaultParameters` to include `container: ''` plus all bucket 1 params with sensible defaults (empty string for text, `false` for booleans that are off by default, `true` for booleans that are on by default). **Always verify exact param names and defaults from the widget's TypeScript types** in `node_modules/instantsearch.js/es/widgets/` or `node_modules/instantsearch.js/es/connectors/` — do not guess from memory (e.g., it's `showSubmit`/`showReset`, not `showSubmitButton`/`showResetButton`). +- Add `fieldOrder` array listing: `'container'`, `'placement'`, then all other params in logical order. +- Add `fieldOverrides` for non-string params: + - Boolean params: `{ type: 'switch', label: '' }` + - Object params with enable/disable + sub-fields: `{ type: 'object', label: '...', defaultValue: {...}, fields: [...] }` - Add `cssClasses` support. Every widget has its own set of CSS classes. Look up the widget's `*CSSClasses` type in the `.d.ts` file to get the exact keys. Add it as: - `defaultParameters`: `cssClasses: false` (collapsed by default) - `fieldOrder`: add `'cssClasses'` at the end - `fieldOverrides`: use the `object` type with all CSS class keys as string fields (see `ais.searchBox` for an example) - `paramDescriptions`: one sentence describing what it customizes -- Add `paramLabels` for string parameters that need human-readable labels (skip booleans, their label comes from `fieldOverrides`). -- Add `paramDescriptions` for all bucket 1 parameters and `cssClasses` (one sentence each, shown as info tooltips next to field labels in the toolbar UI and used by the AI assistant, so write them for end users). +- Add `paramLabels` for string params that need human-readable labels (skip booleans — their label comes from `fieldOverrides`). +- Add `paramDescriptions` for all bucket 1 params and `cssClasses` (one sentence each — shown as info tooltips next to field labels in the toolbar UI and used by the AI assistant, so write them for end users). - Keep the existing `icon` as-is. ### 4. Verify diff --git a/packages/runtime/src/experiences/types.ts b/packages/runtime/src/experiences/types.ts index 5652165..6303fe3 100644 --- a/packages/runtime/src/experiences/types.ts +++ b/packages/runtime/src/experiences/types.ts @@ -4,6 +4,7 @@ import type { Widget, } from 'instantsearch.js/es/types'; import type { ChatWidget } from 'instantsearch.js/es/widgets/chat/chat'; +import type { SearchBoxWidget } from 'instantsearch.js/es/widgets/search-box/search-box'; export type Environment = 'prod' | 'beta'; @@ -44,5 +45,9 @@ export type ExperienceWidget = Widget & { $$supportedWidgets: { 'ais.chat': SupportedWidget[0]>; 'ais.autocomplete': SupportedWidget; - } & Record<'ais.chat' | 'ais.autocomplete' | (string & {}), SupportedWidget>; + 'ais.searchBox': SupportedWidget[0]>; + } & Record< + 'ais.chat' | 'ais.autocomplete' | 'ais.searchBox' | (string & {}), + SupportedWidget + >; }; diff --git a/packages/runtime/src/experiences/widget.tsx b/packages/runtime/src/experiences/widget.tsx index ecae85b..e897760 100644 --- a/packages/runtime/src/experiences/widget.tsx +++ b/packages/runtime/src/experiences/widget.tsx @@ -5,6 +5,7 @@ import { import { getExperience } from './get-experience'; import chat from 'instantsearch.js/es/widgets/chat/chat'; import { EXPERIMENTAL_autocomplete } from 'instantsearch.js/es/widgets/autocomplete/autocomplete'; +import searchBox from 'instantsearch.js/es/widgets/search-box/search-box'; import { renderTemplate, renderTool } from './renderer'; import type { ExperienceWidget } from './types'; @@ -127,6 +128,14 @@ export default (function experience(widgetParams: ExperienceWidgetParams) { }); }, }, + // TODO: Add support for `templates` (submit, reset, loadingIndicator) + // TODO: Add support for `queryHook` (bucket 3 — function) + 'ais.searchBox': { + widget: searchBox, + async transformParams(params) { + return params; + }, + }, }, render: () => {}, dispose: () => {}, diff --git a/packages/toolbar/src/widget-types.tsx b/packages/toolbar/src/widget-types.tsx index d1067ef..68143c1 100644 --- a/packages/toolbar/src/widget-types.tsx +++ b/packages/toolbar/src/widget-types.tsx @@ -292,10 +292,87 @@ export const WIDGET_TYPES: Record = { }, 'ais.searchBox': { label: 'Search Box', - enabled: false, + enabled: true, + description: 'A search input with submit, reset, and loading indicators.', icon: SEARCH_ICON, defaultParameters: { container: '', + placeholder: '', + autofocus: false, + showLoadingIndicator: true, + showSubmit: true, + showReset: true, + searchAsYouType: true, + ignoreCompositionEvents: false, + cssClasses: false, + }, + fieldOrder: [ + 'container', + 'placement', + 'placeholder', + 'autofocus', + 'searchAsYouType', + 'ignoreCompositionEvents', + 'showLoadingIndicator', + 'showSubmit', + 'showReset', + 'cssClasses', + ], + fieldOverrides: { + autofocus: { type: 'switch', label: 'Autofocus' }, + searchAsYouType: { type: 'switch', label: 'Search as you type' }, + ignoreCompositionEvents: { + type: 'switch', + label: 'Ignore composition events', + }, + showLoadingIndicator: { type: 'switch', label: 'Show loading indicator' }, + showSubmit: { type: 'switch', label: 'Show submit button' }, + showReset: { type: 'switch', label: 'Show reset button' }, + cssClasses: { + type: 'object', + label: 'CSS classes', + defaultValue: { + root: '', + form: '', + input: '', + submit: '', + submitIcon: '', + reset: '', + resetIcon: '', + loadingIndicator: '', + loadingIcon: '', + }, + fields: [ + { key: 'root', label: 'Root' }, + { key: 'form', label: 'Form' }, + { key: 'input', label: 'Input' }, + { key: 'submit', label: 'Submit' }, + { key: 'submitIcon', label: 'Submit Icon' }, + { key: 'reset', label: 'Reset' }, + { key: 'resetIcon', label: 'Reset Icon' }, + { key: 'loadingIndicator', label: 'Loading Indicator' }, + { key: 'loadingIcon', label: 'Loading Icon' }, + ], + }, + }, + paramLabels: { + container: 'Container', + placeholder: 'Placeholder', + }, + paramDescriptions: { + container: + 'CSS selector for the DOM element to render into (e.g. "#search-box").', + placeholder: 'Placeholder text shown in the search input.', + autofocus: 'Whether the input should be focused on page load.', + searchAsYouType: + 'When enabled, triggers a search on each keystroke. When disabled, searches only on submit.', + ignoreCompositionEvents: + 'When enabled, ignores IME composition events for CJK input.', + showLoadingIndicator: + 'Whether to show a loading indicator while results are being fetched.', + showSubmit: 'Whether to show the submit button.', + showReset: 'Whether to show the reset button.', + cssClasses: 'CSS classes to apply to the widget DOM elements.', }, }, 'ais.refinementList': { From 89ad8489b9a9f4813159de30821ab51aa8e5857e Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:18:16 +0100 Subject: [PATCH 3/5] fix(toolbar): update test for enabled ais.searchBox widget Co-Authored-By: Claude Opus 4.6 --- .claude/skills/add-widget/SKILL.md | 30 ++++++++++----------- packages/toolbar/__tests__/ai-tools.test.ts | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.claude/skills/add-widget/SKILL.md b/.claude/skills/add-widget/SKILL.md index 1cebf15..9ff0c20 100644 --- a/.claude/skills/add-widget/SKILL.md +++ b/.claude/skills/add-widget/SKILL.md @@ -4,28 +4,28 @@ Add support for the InstantSearch.js widget `$ARGUMENTS`. ## Reference -Use the [widget parameter buckets research](https://algolia.atlassian.net/wiki/spaces/AE/pages/6749356043/InstantSearch.js+widget+parameters+Bucket+Categorization) to determine which parameters belong to each bucket (serializable, templates, functions) for this widget. +Use the [widget parameter buckets research](https://algolia.atlassian.net/wiki/x/CwBLkgE) to determine which parameters belong to each bucket (serializable, templates, functions) for this widget. ## Steps ### 1. Register the widget in `packages/runtime/src/experiences/widget.tsx` -- Import the widget factory from `instantsearch.js/es/widgets//` (check `node_modules/instantsearch.js/es/widgets/` for the exact path — the directory name often uses hyphens where the widget name uses camelCase, e.g., `searchBox` → `search-box`). +- Import the widget factory from `instantsearch.js/es/widgets//` (check `node_modules/instantsearch.js/es/widgets/` for the exact path. The directory name often uses hyphens, while the widget name uses camelCase, e.g., `searchBox` → `search-box`). - Add the widget key to `$$supportedWidgets` inside the `experience` function, right before the closing `},` of the object. -- For bucket-1-only widgets (all params are serializable), `transformParams` is a passthrough: +- For bucket-1-only widgets (all parameters are serializable), `transformParams` is a passthrough: ```ts 'ais.': { widget: , - async transformParams(params) { - return params; + async transformParams(parameters) { + return parameters; }, }, ``` -- For widgets with bucket 2/3 params that need decomposition, model `transformParams` after the existing `ais.chat` or `ais.autocomplete` entries. +- For widgets with bucket 2/3 parameters that need decomposition, model `transformParams` after the existing `ais.chat` or `ais.autocomplete` entries. - Add TODO comments above the entry listing unsupported parameters by bucket: ```ts // TODO: Add support for `templates` () - // TODO: Add support for `` (bucket 3 — function) + // TODO: Add support for `` (bucket 3 function) ``` Only add TODOs for buckets that have parameters for this widget. Skip if the widget is bucket 1 only. @@ -43,19 +43,19 @@ Use the [widget parameter buckets research](https://algolia.atlassian.net/wiki/s The widget likely already exists in `WIDGET_TYPES` with `enabled: false`. Update it: - Set `enabled: true`. -- Add a `description` string (one sentence describing what the widget does — used by the AI assistant). -- Set `defaultParameters` to include `container: ''` plus all bucket 1 params with sensible defaults (empty string for text, `false` for booleans that are off by default, `true` for booleans that are on by default). **Always verify exact param names and defaults from the widget's TypeScript types** in `node_modules/instantsearch.js/es/widgets/` or `node_modules/instantsearch.js/es/connectors/` — do not guess from memory (e.g., it's `showSubmit`/`showReset`, not `showSubmitButton`/`showResetButton`). -- Add `fieldOrder` array listing: `'container'`, `'placement'`, then all other params in logical order. -- Add `fieldOverrides` for non-string params: - - Boolean params: `{ type: 'switch', label: '' }` - - Object params with enable/disable + sub-fields: `{ type: 'object', label: '...', defaultValue: {...}, fields: [...] }` +- Add a `description` string (one sentence describing what the widget does, used by the AI assistant). +- Set `defaultParameters` to include `container: ''` plus all bucket 1 parameters with sensible defaults (empty string for text, `false` for booleans that are off by default, `true` for booleans that are on by default). **Always verify exact parameter names and defaults from the widget's TypeScript types** in `node_modules/instantsearch.js/es/widgets/` or `node_modules/instantsearch.js/es/connectors/`, do not guess from memory (e.g., it's `showSubmit`/`showReset`, not `showSubmitButton`/`showResetButton`). +- Add `fieldOrder` array listing: `'container'`, `'placement'`, then all other parameters in logical order. +- Add `fieldOverrides` for non-string parameters: + - Boolean parameters: `{ type: 'switch', label: '' }` + - Object parameters with enable/disable + sub-fields: `{ type: 'object', label: '...', defaultValue: {...}, fields: [...] }` - Add `cssClasses` support. Every widget has its own set of CSS classes. Look up the widget's `*CSSClasses` type in the `.d.ts` file to get the exact keys. Add it as: - `defaultParameters`: `cssClasses: false` (collapsed by default) - `fieldOrder`: add `'cssClasses'` at the end - `fieldOverrides`: use the `object` type with all CSS class keys as string fields (see `ais.searchBox` for an example) - `paramDescriptions`: one sentence describing what it customizes -- Add `paramLabels` for string params that need human-readable labels (skip booleans — their label comes from `fieldOverrides`). -- Add `paramDescriptions` for all bucket 1 params and `cssClasses` (one sentence each — shown as info tooltips next to field labels in the toolbar UI and used by the AI assistant, so write them for end users). +- Add `paramLabels` for string parameters that need human-readable labels (skip booleans, their label comes from `fieldOverrides`). +- Add `paramDescriptions` for all bucket 1 parameters and `cssClasses` (one sentence each, shown as info tooltips next to field labels in the toolbar UI and used by the AI assistant, so write them for end users). - Keep the existing `icon` as-is. ### 4. Verify diff --git a/packages/toolbar/__tests__/ai-tools.test.ts b/packages/toolbar/__tests__/ai-tools.test.ts index 2006235..d8a59d4 100644 --- a/packages/toolbar/__tests__/ai-tools.test.ts +++ b/packages/toolbar/__tests__/ai-tools.test.ts @@ -51,7 +51,7 @@ describe('describeWidgetTypes', () => { it('excludes disabled widget types', () => { const result = describeWidgetTypes(); expect(result).not.toContain('ais.hits'); - expect(result).not.toContain('ais.searchBox'); + expect(result).toContain('ais.searchBox'); expect(result).not.toContain('ais.pagination'); }); }); From 496320c0eaa526a72b85123f484f2f566c7f0d4b Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:00:22 +0100 Subject: [PATCH 4/5] test(toolbar): improve searchBox test coverage in AI tools Co-Authored-By: Claude Opus 4.6 --- packages/toolbar/__tests__/ai-tools.test.ts | 97 ++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/toolbar/__tests__/ai-tools.test.ts b/packages/toolbar/__tests__/ai-tools.test.ts index d8a59d4..86ba56b 100644 --- a/packages/toolbar/__tests__/ai-tools.test.ts +++ b/packages/toolbar/__tests__/ai-tools.test.ts @@ -30,6 +30,7 @@ describe('describeWidgetTypes', () => { expect(result).toContain('Autocomplete'); expect(result).toContain('ais.chat'); expect(result).toContain('Chat'); + expect(result).toContain('ais.searchBox'); }); it('includes widget descriptions and parameter descriptions', () => { @@ -51,7 +52,6 @@ describe('describeWidgetTypes', () => { it('excludes disabled widget types', () => { const result = describeWidgetTypes(); expect(result).not.toContain('ais.hits'); - expect(result).toContain('ais.searchBox'); expect(result).not.toContain('ais.pagination'); }); }); @@ -433,6 +433,38 @@ describe('getTools', () => { expect(callbacks.onAddBlock).toHaveBeenCalledWith('ais.autocomplete'); }); + it('adds searchBox with default inside placement and container', async () => { + const experience: ExperienceApiResponse = { blocks: [] }; + const callbacks = createCallbacks(experience); + const tools = getTools(callbacks); + + const result = await tools.add_widget.execute!( + { type: 'ais.searchBox', container: '#search-box' }, + { toolCallId: 'tc1', messages: [] } + ); + + expect(callbacks.onAddBlock).toHaveBeenCalledWith('ais.searchBox'); + expect(callbacks.onParameterChange).toHaveBeenCalledWith( + 0, + 'placement', + 'inside' + ); + expect(callbacks.onParameterChange).toHaveBeenCalledWith( + 0, + 'container', + '#search-box' + ); + expect(result).toMatchObject({ + success: true, + index: 0, + type: 'ais.searchBox', + placement: 'inside', + container: '#search-box', + applied: ['placement', 'container'], + rejected: [], + }); + }); + it('computes the correct index for non-empty experiences', async () => { const experience: ExperienceApiResponse = { blocks: [ @@ -764,6 +796,69 @@ describe('getTools', () => { error: expect.stringContaining('indices 0–1'), }); }); + + it('applies a boolean parameter with switch override on searchBox', async () => { + const experience: ExperienceApiResponse = { + blocks: [ + { + type: 'ais.searchBox', + parameters: { container: '#search-box', searchAsYouType: true }, + }, + ], + }; + const callbacks = createCallbacks(experience); + const tools = getTools(callbacks); + + const result = await tools.edit_widget.execute!( + { index: 0, parameters: { searchAsYouType: false } }, + { toolCallId: 'tc1', messages: [] } + ); + + expect(result).toMatchObject({ + success: true, + applied: ['searchAsYouType'], + rejected: [], + }); + expect(callbacks.onParameterChange).toHaveBeenCalledWith( + 0, + 'searchAsYouType', + false + ); + }); + + it('applies an object parameter (cssClasses) on searchBox', async () => { + const experience: ExperienceApiResponse = { + blocks: [ + { + type: 'ais.searchBox', + parameters: { container: '#search-box', cssClasses: false }, + }, + ], + }; + const callbacks = createCallbacks(experience); + const tools = getTools(callbacks); + + const result = await tools.edit_widget.execute!( + { + index: 0, + parameters: { + cssClasses: { root: 'my-root', input: 'my-input' }, + }, + }, + { toolCallId: 'tc1', messages: [] } + ); + + expect(result).toMatchObject({ + success: true, + applied: ['cssClasses'], + rejected: [], + }); + expect(callbacks.onParameterChange).toHaveBeenCalledWith( + 0, + 'cssClasses', + { root: 'my-root', input: 'my-input' } + ); + }); }); describe('remove_widget', () => { From 82dec9331da9386964f7626cbb6727a825d5537c Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:31:11 +0100 Subject: [PATCH 5/5] chore: retrigger CI Co-Authored-By: Claude Opus 4.6