Converse.js Agent Guidelines

Converse.js is a modern, feature-rich XMPP chat client that runs in web browsers. It's a plugin-based architecture written in JavaScript with TypeScript type definitions, using the Lit framework for UI components.

Project Overview

Workspace Structure

This is a monorepo with npm workspaces:

Essential Commands

Development

# Development build (unminified, with debugger statements)
npm run dev                    # Build everything in dev mode
npm run dev:headless          # Build only headless package

# Watch mode (auto-rebuild on changes)
npm run watch                 # Watch both headless and main
npm run watch:headless        # Watch only headless
npm run watch:main            # Watch only main package

# Dev server with live reload
npm run devserver             # Starts on http://localhost:8080

# Serve static files
npm run serve                 # Serves on http://localhost:8000 (default)
npm run serve -- -p 8008      # Serve on custom port
npm run serve-tls             # HTTPS server (requires certs/)

Building

# Production build (minified)
npm run build                 # Full build: headless + ESM + CJS + CSS
npm run build:headless        # Build headless package only
npm run build:esm             # Build ESM bundle
npm run build:cjs             # Build CommonJS bundle
npm run build:website-css     # Build website CSS
npm run build:website-min-css # Build and minify website CSS

# Special builds
npm run nodeps                # Build without dependencies
npm run cdn                   # Build for CDN deployment

Testing

# Run tests
npm test                      # Run main tests (Karma)
npm run test:all              # Run both headless and main tests
npm run test:headless         # Run headless tests only
cd src/headless && npm test   # Alternative way to run headless tests

# Single run (for CI)
npm test -- --single-run
npm run test:all              # Already includes --single-run

# Full test suite (as used in CI)
make check                    # Runs lint + types + all tests

Code Quality

# Linting
npm run lint                  # Run ESLint on all source files

# Type checking
npm run types:check           # TypeScript type checking (no emit)
npm run types                 # Generate type definitions

# Clean
npm run clean                 # Remove node_modules, dist, builds

Make Targets (Alternative)

make dev                      # Same as npm run dev
make devserver                # Same as npm run devserver
make watch                    # Same as npm run watch
make check                    # Lint + types + tests (full CI suite)
make test                     # Run tests
make test-headless            # Run headless tests
make serve                    # Serve on port 8008

Architecture

Plugin System

Converse.js uses a plugin-based architecture powered by pluggable.js:

Plugin Structure

Every plugin follows this pattern:

import { _converse, api, converse } from '@converse/headless';

converse.plugins.add('plugin-name', {
    dependencies: ['other-plugin-1', 'other-plugin-2'], // Required plugins
    
    initialize () {
        // Configure plugin settings
        api.settings.extend({
            'some_setting': 'default_value',
        });
        
        // Export models/views for other plugins
        const exports = { MyClass, myFunction };
        Object.assign(_converse, exports);        // DEPRECATED pattern
        Object.assign(_converse.exports, exports); // Current pattern
        
        // Extend API
        Object.assign(api, my_api_methods);
        
        // Register event listeners
        api.listen.on('connected', () => { /* ... */ });
    }
});

Directory Structure

src/
├── headless/             # Core XMPP logic (separate package)
│   ├── plugins/          # Headless plugins (chat, muc, roster, etc.)
│   ├── shared/           # Shared headless utilities
│   ├── types/            # Generated TypeScript definitions
│   └── dist/             # Built headless package
├── plugins/              # UI plugins
│   ├── chatview/         # Chat UI
│   ├── muc-views/        # Multi-user chat UI
│   ├── rosterview/       # Contact list UI
│   └── controlbox/       # Main control panel
├── shared/               # Shared UI components
│   ├── components/       # Reusable Lit components
│   ├── chat/             # Chat-related shared components
│   ├── modals/           # Modal dialogs
│   └── styles/           # Shared SCSS files
├── templates/            # Lit template functions
├── i18n/                 # Internationalization
│   └── locales/          # Translation files (.po)
├── types/                # Generated TypeScript definitions
└── utils/                # Utility functions

State Management

Converse.js uses a custom fork of Backbone.js called @converse/skeletor for state management by means of Models and Collections of Models.

Key Concepts

Why Skeletor?

As Converse.js evolved, the team created @converse/skeletor as a fork of Backbone.js to:

Working with Models

// Creating a model instance
const chatroom = new _converse.exports.ChatRoom({
    jid: 'room@muc.example.com',
    nick: 'user123'
});

// Accessing model attributes
const jid = chatroom.get('jid');

// Listening to model changes
chatroom.on('change:subject', () => {
    console.log('Room subject changed');
});

// Saving model changes
chatroom.save({'subject': 'New Subject'});

Working with Collections

// Accessing the chatboxes collection
const chatboxes = _converse.state.chatboxes;

// Finding a specific model in a collection
const chatbox = chatboxes.get('user@example.com');

// Adding a model to a collection
chatboxes.add(new _converse.ChatBox({...}));

// Listening to collection events
chatboxes.on('add', (chatbox) => {
    console.log('New chatbox added:', chatbox.get('jid'));
});

Integration with Lit Components

While UI components use Lit, they integrate with Skeletor models:

class ChatRoomView extends CustomElement {
    async initialize() {
        // Wait for model to be ready
        await this.model.initialized;
        
        // Listen to model changes to trigger re-render
        this.listenTo(this.model, 'change', () => this.requestUpdate());
        this.listenTo(this.model.messages, 'add', () => this.requestUpdate());
        
        this.requestUpdate();
    }
    
    render() {
        return html`
            <div class="chatroom">
                <header>${this.model.get('name')}</header>
                <!-- Render messages, participants, etc. -->
            </div>
        `;
    }
}

Code Style and Conventions

Formatting (Prettier)

{
  "singleQuote": true,           // Use single quotes
  "printWidth": 120,             // Max line length 120 chars
  "tabWidth": 4,                 // 4-space indentation
  "useTabs": false,              // Spaces, not tabs
  "spaceBeforeFunctionParen": true  // function () not function()
}

Naming Conventions

Import Patterns

// Headless core imports
import { _converse, api, converse } from '@converse/headless';

// Logging
import { log } from '@converse/log';

// Lit framework
import { html, css } from 'lit';

// Relative imports for local files
import ChatView from './chat.js';
import './styles/index.scss';

// Utilities
import { u } from '@converse/headless'; // Utility functions
const { dayjs, Strophe, sizzle } = converse.env; // Common libraries

Component Patterns

Lit Components extend CustomElement:

import { html } from 'lit';
import { CustomElement } from 'shared/components/element.js';

export default class MyComponent extends CustomElement {
    static get properties() {
        return {
            model: { type: Object },
            some_state: { state: true }, // Internal state
        };
    }
    
    async initialize() {
        await this.model.initialized;
        this.listenTo(this.model, 'change', () => this.requestUpdate());
        this.requestUpdate();
    }
    
    render() {
        return html`<div>...</div>`;
    }
}
customElements.define('my-component', MyComponent);

Templates are functions returning html tagged templates:

import { html } from 'lit';

export default (model) => html`
    <div class="chat-message">
        <span>${model.get('from')}</span>
        <p>${model.get('body')}</p>
    </div>
`;

API Usage Patterns

// Settings
api.settings.extend({ 'my_setting': 'default' });
api.settings.get('my_setting');

// Events
api.listen.on('connected', callback);
api.trigger('customEvent', data);

// Promises
await api.waitUntil('connected');

// Disco features
api.disco.own.features.add(Strophe.NS.SPOILER);

// User interaction
const confirmed = await api.confirm('Are you sure?');
await api.alert('Something happened');

TypeScript and Type Definitions

Configuration

JSDoc for Types

Add JSDoc comments to document types in .js files:

/**
 * @typedef {Object} MessageAttributes
 * @property {string} body - Message body text
 * @property {string} from - Sender JID
 * @property {string} type - Message type
 */

/**
 * @param {string} jid - The JID to check
 * @returns {Promise<boolean>}
 */
async function isContact(jid) {
    // ...
}

Type Checking

npm run types:check  # Check types without generating files
npm run types        # Generate type definitions

Testing

Test Structure

Test Patterns

/*global mock, converse */

const { api } = converse;
const u = converse.env.utils;
const sizzle = converse.env.sizzle;

describe("My Feature", function () {
    it("does something", mock.initConverse(['chatBoxesFetched'], 
        { view_mode: 'fullscreen' }, 
        async function (_converse) {
            // Setup
            await mock.waitForRoster(_converse, 'current', 1);
            await mock.openControlBox(_converse);
            
            // Test action
            const jid = 'user@example.com';
            await mock.openChatBoxFor(_converse, jid);
            const view = _converse.chatboxviews.get(jid);
            
            // Assertions
            await u.waitUntil(() => sizzle('.chat-msg', view).length === 1);
            expect(view.querySelector('.chat-msg__text').textContent).toBe('hello');
        }
    ));
});

Running Specific Tests

To run a specific test file, add it to the files array in karma.conf.js:

files: [
    // ... existing files
    { pattern: "src/plugins/my-plugin/tests/my-test.js", type: 'module' },
],

Or run headless tests:

cd src/headless
npm test  # Runs karma with src/headless/karma.conf.js

Styling

SCSS Organization

Style Patterns

// Import Bootstrap utilities
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";

// Component styles
.chat-content {
    &__messages {
        overflow-y: auto;
    }
    
    &__notifications {
        padding: 1rem;
    }
}

CSS Loading

Styles are imported directly in JavaScript:

import './styles/chat-content.scss';

Rspack uses style-loader + css-loader + postcss-loader + sass-loader to process and inject styles.

Internationalization (i18n)

Translation System

Using Translations

import { __ } from '@converse/headless';

const message = __('Hello, %1$s!', username);
const plural = __('%1$d messages', count);

Translation Workflow

# Extract strings from source
npm run nodeps  # Builds converse-no-dependencies.js
make pot        # Generates src/i18n/converse.pot

# Update existing translations
make po         # Merges pot into all locale .po files

Build System (Rspack)

Configuration Files

Important Loaders

Environment Variables

DROP_DEBUGGER=true     # Remove debugger statements (production)
ASSET_PATH=/dist/      # Public path for assets (default)
ASSET_PATH=https://cdn.conversejs.org/dist/  # CDN path

Common Patterns and Gotchas

1. Async Initialization

Models and collections are initialized asynchronously:

await this.model.initialized;        // Wait for model
await this.model.messages.fetched;   // Wait for data fetch

2. Event Listening

Use Backbone-style event listeners (automatically cleaned up):

this.listenTo(this.model, 'change', () => this.requestUpdate());
this.listenTo(this.model.messages, 'add', this.onMessageAdded);

3. Waiting for Conditions

Use utility functions to wait:

await u.waitUntil(() => sizzle('.chat-msg', view).length > 0);
await api.waitUntil('connected');

4. Accessing Converse Internals

import { _converse, api, converse } from '@converse/headless';

// Access global state via `_converse.state` (use sparingly, prefer api)
const { chatboxes } = _converse.state;
const chatbox = chatboxes.get(jid);

// Access 3rd party libraries
const { Strophe, $msg, $iq, $pres, $build, stx } = converse.env;

// XML stanzas (prefer stx template literal over old $msg, $pres, $iq, $build functions)
// The stx template literal is the current preferred method for creating XML stanzas
// When using stx, the `xmlns` attribute always needs to be set to "jabber:client".
const stanza_stx = stx`
    <message to="${jid}" type="chat" xmlns="jabber:client">
        <body>Hello</body>
        <active xmlns="${Strophe.NS.CHATSTATES}"/>
    </message>`;

// Legacy methods ($msg, $pres, $iq, $build) are deprecated - use stx instead
const stanza_legacy = $msg({ to: jid, type: 'chat' })
    .c('body').t('Hello').up()
    .c('active', { xmlns: Strophe.NS.CHATSTATES });

5. Custom Elements

Always define custom elements:

class MyElement extends CustomElement { /* ... */ }
customElements.define('my-element', MyElement);

Use in HTML:

html`<my-element .model="${this.model}"></my-element>`

6. Lit Property Binding

// Set properties with . prefix
.model="${this.model}"

// Set attributes with regular syntax
id="my-id"
class="my-class"

// Boolean attributes with ?
?disabled="${this.isDisabled}"

// Event handlers with @
@click="${this.handleClick}"

7. Memory Leaks Prevention

8. ESLint Rules

CI/CD

GitHub Actions

CI Test Command

make check  # Runs: eslint + types + tests (headless + main)

This command:

  1. Runs npm run lint (ESLint)
  2. Runs npm run types (generate types)
  3. Checks for uncommitted type changes
  4. Runs headless tests: cd src/headless && npm run test -- --single-run
  5. Runs main tests: npm run test -- --single-run

Debugging

Development Tools

// Access global converse object in browser console
window.converse

// Internal state (as represented by Models and Collections)
_converse.state

// Namespace for storing code that might be useful to 3rd party
// plugins. We want to make it possible for 3rd party plugins to have
// access to code (e.g. classes) without having to add converse.js
// as a dependency.
_converse.exports 

// API methods
const { api } = _converse;
api.user.jid()
api.settings.get('jid')
api.chatboxes.get('user@example.com')

Debug Logging

import { log } from '@converse/log';

log.debug('Debug message');
log.info('Info message');
log.warn('Warning message');
log.error('Error message');

Set log level in settings:

converse.initialize({
    loglevel: 'debug', // 'debug', 'info', 'warn', 'error'
});

Common Issues

  1. Import errors: Check path aliases in rspack.common.js resolve section
  2. Style not applying: Make sure SCSS import is at top of plugin entry file
  3. Component not rendering: Check customElements.define() is called
  4. Tests failing: Ensure await mock.waitForRoster() and other setup completes
  5. Type errors: Run npm run types to regenerate definitions

Release Process

See Makefile and RELEASE.md for full details:

# Update version in all files
make version VERSION=12.1.0

# Create release
make publish BRANCH=master

# Post-release (bump to dev version)
make postrelease VERSION=12.1.0

Documentation

Generate docs:

make docsdev  # Install Python dependencies
make doc      # Build HTML documentation

Key Files to Know

Additional Resources