Skip to content

Conversation

@pranavjoshi001
Copy link

@pranavjoshi001 pranavjoshi001 commented Jan 26, 2026

Changelog Entry

  • Added pull-based capabilities system for dynamically discovering adapter capabilities at runtime, in PR #5679, by @pranavjoshi001

Description

Introduces a Capabilities system that enables WebChat to dynamically discover and consume capabilities from chat adapters at runtime.

This allows adapters(directline, ccv2 etc..) to expose configuration such as voice settings without requiring WebChat to have compile-time knowledge of specific adapter implementations. The system follows a pull-based model where adapters expose getter functions and emit events when capabilities change.

Design

Pull-Based Architecture

  • Adapters expose getters: Adapters implement getter functions (e.g., getVoiceConfiguration()) to provide capability values
  • Event-driven updates: Adapters emit capabilitiesChanged events as a "nudge" to signal WebChat should re-fetch capabilities
  • Registry-based discovery: A capability registry maps capability keys to getter function names, making the system extensible

Pure Derivation Pattern

  • Uses useReduceMemo to derive capabilities from the activity stream
  • A synthetic init marker ensures initial fetch happens on mount
  • Capabilities are only re-fetched when:
    1. A capabilitiesChanged event is detected in activities

Reference Stability

  • Individual capability objects maintain reference equality when unchanged
  • Consumers using selectors only re-render when their specific capability changes
  • Uses shallow equality comparison with support for custom comparators

Specific Changes

  • Added CapabilitiesComposer component that provides capabilities context
  • Added useCapabilities(selector) hook for consuming capabilities with selector pattern
  • Added fetchCapabilitiesFromAdapter utility to pull capabilities from adapter getters
  • Added capabilityRegistry to map capability keys to adapter getter names
  • Added Capabilities and VoiceConfiguration types
  • Added valibot schema validation for capabilitiesChanged events
  • Exported useCapabilities hook from botframework-webchat/hook
  • Added setCapability(getterName, value, options) helper to test emulator
  • Added useCapabilities.html test covering initial fetch, event-driven updates, and reference stability
  • Added CAPABILITIES.md documentation
  • I have added tests and executed them locally
  • I have updated CHANGELOG.md
  • I have updated documentation

Review Checklist

This section is for contributors to review your work.

  • Accessibility reviewed (tab order, content readability, alt text, color contrast)
  • Browser and platform compatibilities reviewed
  • CSS styles reviewed (minimal rules, no z-index)
  • Documents reviewed (docs, samples, live demo)
  • Internationalization reviewed (strings, unit formatting)
  • package.json and package-lock.json reviewed
  • Security reviewed (no data URIs, check for nonce leak)
  • Tests reviewed (coverage, legitimacy)

Comment on lines +25 to +27
| Capability | Type | Description |
|------------|------|-------------|
| `voiceConfiguration` | `{ sampleRate: number, chunkIntervalMs: number }` | Audio settings for Speech-to-Speech |
Copy link
Contributor

@compulim compulim Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Format and sort.

@@ -0,0 +1,85 @@
# Capabilities

Web Chat supports dynamic capability discovery from adapters. Capabilities allow adapters to expose configuration values that WebChat components can consume and react to changes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Web Chat supports dynamic capability discovery from adapters. Capabilities allow adapters to expose configuration values that WebChat components can consume and react to changes.
Web Chat supports dynamic capability discovery from adapters. Capabilities allow adapters to expose configuration values that Web Chat components can consume and react to changes.

const prevValue = prevCapabilities[key];

if (fetchedValue) {
if (prevValue !== undefined && isEqual(prevValue, fetchedValue)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it just happened no one assigned window.undefined = 1.

Suggested change
if (prevValue !== undefined && isEqual(prevValue, fetchedValue)) {
if (typeof prevValue !== 'undefined' && isEqual(prevValue, fetchedValue)) {

@@ -0,0 +1,17 @@
import { isForbiddenPropertyName } from 'botframework-webchat-core';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment.

// TODO: [P2] Move this file to `base` packagee

{
key: 'voiceConfiguration',
getterName: 'getVoiceConfiguration'
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later. Add a Valibot schema.

* - This ensures consumers using selectors only re-render when their capability changes
*/
const CapabilitiesComposer = memo(({ children }: Props) => {
const [activities] = useActivities();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very soon, we need to move to directLine.addEventListener('capabilitieschange') instead of waiting for "event activities with type capabilitiesChanged".

),
EMPTY_CAPABILITIES
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sooner, update to use EventTarget than activity$.

const CapabilitiesComposer = ({ chatAdapter, children }) => {
  function getAllCapabilities() { ... } // Call getVoiceConfiguration() and return as { voiceConfiguration: {} }.

  const [value, setValue] = useState(() => getAllCapabilities(chatAdapter), [chatAdapter]);

  useEffect(() => { 
    const handleCapabilitiesChange = () => {
      setValue(getAllCapabilities(chatAdapter)); // Setter in useEffect is unevitable for EventTarget.
    };

    chatAdapter.addEventListener('capabilitieschange', handleCapabilitiesChange);

    return () => chatAdapter.removeEventListener('capabilitieschange', handleCapabilitiesChange);
  }, [chatAdapter]);

  return <Context value={value}>{children}</Context>;
};

Copy link
Contributor

@compulim compulim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coupling between this PR and the S2S PR:

  • showMicrophoneButton
  • chunkDurationInMs/sampleRate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants