Thanks for your interest in contributing! This guide will help you get started.
- Code of Conduct
- Getting Started
- Development Setup
- Architecture Overview
- Making Changes
- Testing
- Pull Request Process
- Style Guide
- Reporting Bugs
- Requesting Features
Be respectful, constructive, and inclusive. We're all here to build something useful.
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/<your-username>/agenticchat.git cd agenticchat
- Create a branch for your work:
git checkout -b feature/your-feature-name
Agentic Chat is a zero-dependency browser application. The core app (index.html, app.js, style.css) requires no build tools — just open index.html in a browser.
For running tests:
npm install # Install dev dependencies (Jest)
npm test # Run the test suite- Node.js ≥ 18.0.0 (for tests only)
- A modern browser (Chrome, Firefox, Safari, Edge)
- An OpenAI API key with GPT-4o access (for manual testing)
The codebase is a single-file application (app.js, ~60k LOC) composed of many self-contained IIFE modules. For a full architectural deep-dive see ARCHITECTURE.md. The core modules are:
| Module | Responsibility |
|---|---|
ChatConfig |
Constants and configuration (model, limits, prompts) |
ConversationManager |
Chat history management, trimming, token estimation |
SandboxRunner |
iframe sandbox creation, code execution, cancellation |
ApiKeyManager |
OpenAI key storage, per-service key prompts, modal handling |
UIController |
All DOM updates, button state, character counter |
ChatController |
Orchestrates message flow, API calls, response processing |
OpenAIClient |
Streaming SSE fetch wrapper, error mapping |
SafeStorage / sanitizeStorageObject |
Trust boundary for localStorage (prototype-pollution defence) |
_escapeHtml |
Canonical HTML escape used everywhere strings hit innerHTML |
ChatOutputObserver |
Shared MutationObserver for #chat-output — register a callback instead of spawning your own observer |
TextAnalysisUtils |
Shared tokenization, stop-words, jaccard, cosine, TF helpers |
| Agentic siblings | ReplyStyleAdvisor, AssistantConfidenceCalibrator, ConversationHealthMonitor, PromptClarityCoach, PrivacyLeakDetector, ToolCallSafetyAdvisor, OpenLoopTracker, ContextWindowOptimizer, … (search app.js for Add / feat(app): history) |
All modules communicate through public APIs. DOM manipulation is isolated to UIController except where the sandbox requires direct access.
Three primitives form the security perimeter — regressions here become app-wide XSS or prototype pollution:
_escapeHtml(str)— escapes& < > " 'in one pass. Use it for every value interpolated intoinnerHTML/ template literals that produce HTML. Localesc()helpers in submodules must escape the same five characters (single-quote included) so they remain safe in both"..."and'...'attribute contexts.sanitizeStorageObject(obj)— strips__proto__/constructor/prototypekeys recursively. EveryJSON.parsefromlocalStorageor imported conversations must go through it (or via_safeParse)._safeParse(raw, fallback)— one-shotJSON.parse+ sanitize + fallback. Prefer this over rawJSON.parsefor any persisted or imported data.
The focused regression suite is tests/security-primitives.test.js. Add cases there when you touch any of the three.
├── index.html # Entry point — single HTML file
├── app.js # All application logic (~60k LOC, modular IIFEs)
├── sw.js # Service worker (offline cache + navigation fallback)
├── style.css # Styles
├── manifest.json # PWA manifest
├── ARCHITECTURE.md # Deep-dive architecture notes
├── tests/ # 90+ focused Jest suites (one per module)
│ ├── setup.js # Shared jsdom + global stubs (always loaded)
│ ├── security-primitives.test.js # _escapeHtml / sanitizeStorageObject / _safeParse
│ ├── app.test.js # Core ChatController / ConversationManager coverage
│ └── <module>.test.js # One file per agentic / utility module
├── scripts/
│ └── stamp-sw.js # Builds versioned CACHE_NAME into sw.js
├── docs/ # GitHub Pages documentation site
├── codecov.yml # Codecov thresholds
├── .c8rc.json # c8 coverage config
├── jest.config.js # Jest config (jsdom env, setupFiles → tests/setup.js)
├── Dockerfile # Container build (nginx-alpine, SBOM + provenance)
└── .github/
├── copilot-setup-steps.yml # Bootstraps Copilot coding agents
├── copilot-instructions.md # Repo-specific guidance for AI agents
└── workflows/ # CI, CodeQL, Docker, Pages, npm publish, labeler, stale
- Check existing issues to avoid duplicate work
- For large changes, open an issue first to discuss the approach
- Keep changes focused — one feature or fix per PR
- Read the code first. Understand the module structure before modifying.
- No new runtime dependencies. The app is intentionally zero-dependency in the browser. Dev dependencies (testing) are fine.
- Security matters. Sandbox isolation, CSP headers, and nonce validation are critical. Don't weaken them.
- Keep it simple. This is a lightweight tool, not a framework.
Tests use Jest with jsdom environment:
npm test # Run all tests (jest --verbose)
npx jest --watch # Watch mode during development
npm run test:coverage # c8 + jest, full coverage report (text + html + lcov)
npm run test:coverage:ci # Same, but lean reporters for CI
npm run coverage:open # Open the generated coverage/index.html
npx jest tests/app.test.js # Run specific test file
npx jest -t 'escapes the five HTML' # Run a single named test💡 Heads-up on the full suite.
npm testwalks all 90+ suites and exercises a lot of jsdom code paths; on cold machines it can take several minutes. While developing, scope to the suite(s) you're touching (npx jest tests/<file>.test.js) and only run the full suite before pushing. CI runs the full suite for you.
- Place test files in
tests/with.test.jsextension - Use
tests/setup.jsfor shared DOM mocks and fixtures - Test behavior, not implementation details
- Cover edge cases and error paths, not just happy paths
Example:
describe('ConversationManager', () => {
test('should trim history when exceeding max pairs', () => {
// Arrange: fill history beyond MAX_HISTORY_PAIRS
// Act: add one more message
// Assert: oldest messages were removed
});
});- Ensure tests pass:
npm testmust succeed locally (or at minimum, the suites your change touches; CI runs the full matrix). New behaviour needs a new test in the matchingtests/<module>.test.js— don't pile assertions intoapp.test.js. - Write descriptive commits: Use clear, imperative commit messages
- ✅
Fix sandbox timeout not clearing on cancel - ✅
Add input validation for API key format - ❌
Fixed stuff - ❌
Update app.js
- ✅
- Update documentation if your change affects usage or behavior
- Keep PRs small — easier to review, faster to merge
- Fill in the PR template (if present) with context on what and why
- Does it maintain security guarantees (sandbox isolation, CSP, nonce validation)?
- Are there tests for new behavior?
- Does it follow the existing code style?
- Is the change necessary and well-scoped?
This project follows Conventional Commits for clear, machine-readable history:
<type>(<scope>): <description>
| Type | When to Use |
|---|---|
feat |
New user-facing feature |
fix |
Bug fix |
perf |
Performance improvement |
refactor |
Code restructuring without behavior change |
test |
Adding or updating tests |
docs |
Documentation only |
ci |
CI/CD pipeline changes |
chore |
Tooling, config, housekeeping |
security |
Security fix or hardening |
Common scopes (optional, lowercase, matching what the git log already uses):
app, sandbox, api, ui, config, tests, sw (service worker), docs,
deps, docker, shortcuts, pages, ci, codeql, labeler, plus the name
of a specific agentic module when the change is module-local
(e.g. replystyleadvisor, deadline-tracker).
Examples:
feat(sandbox): add execution timeout configuration
fix(ui): prevent double-submit on rapid Enter key
perf(api): batch token estimation for multi-turn history
security(sandbox): strengthen CSP nonce validation
- Use
'use strict'(already set globally) - Use
constby default,letwhen reassignment is needed, nevervar - Use descriptive names:
handleUserInputnothUI - Keep functions focused — one responsibility per function
- Add JSDoc comments for public module APIs
- Use
Object.freeze()for configuration objects - Prefer early returns over deeply nested conditionals
- Use
??(nullish coalescing) over||when0or''are valid values - Avoid magic numbers — define constants in
ChatConfig
- Use CSS custom properties (variables) for theming values
- Follow existing naming conventions
- Mobile-first: ensure changes work on small screens
- Group properties: positioning → box model → typography → visual → misc
- Semantic elements where appropriate
- Accessible: labels, ARIA attributes, keyboard navigation
- All interactive elements must be keyboard-reachable (
tabindex, focus styles)
Open an issue with:
- Summary — One sentence describing the bug
- Steps to reproduce — Exact steps to trigger the problem
- Expected behavior — What should happen
- Actual behavior — What actually happens
- Environment — Browser, OS, any relevant extensions
- Screenshots — If applicable
Open an issue with:
- Problem statement — What limitation or need does this address?
- Proposed solution — How would it work from a user perspective?
- Alternatives considered — What else did you think about?
- Scope — Is this a small tweak or a significant change?
This repo is configured for GitHub Copilot coding agents (Claude, Codex). If you're assigned an issue or want to experiment:
- The
.github/copilot-setup-steps.ymlhandles dependency installation and build automatically .github/copilot-instructions.mdprovides repo-specific architecture context to the agent- AI agents can open PRs against issues — review them like any other PR
- When reviewing AI-generated PRs, pay extra attention to:
- Sandbox security — any changes to iframe isolation or CSP must be scrutinized
- Zero-dependency policy — agents sometimes add runtime dependencies
- Test coverage — ensure new behavior has corresponding tests
If you discover a security vulnerability, do not open a public issue. Instead, email the maintainer directly or use GitHub's private vulnerability reporting. Security issues in the sandbox isolation, CSP enforcement, or API key handling are especially critical.
- Quick start: Open
index.htmldirectly withfile://protocol — no server needed for basic development. - Live reload: Use any static server with watch mode (e.g.,
npx serve .or VS Code Live Server extension). - Debugging sandbox: The iframe sandbox uses
srcdoc— inspect it via browser DevTools → Elements → find the<iframe>and switch to its context in the console. - API mocking: For testing without burning API credits, mock
fetchin your test setup (seetests/setup.jsfor examples). - Service Worker: Changes to
sw.jsrequire clearing the SW cache in DevTools → Application → Service Workers → Unregister, then hard refresh. - Service Worker cache stamp: When you change
index.html,app.js, orstyle.css, re-runnpm run build:swand commit the updatedsw.js. CI runsnode scripts/stamp-sw.js --checkand will fail the build if the stamp is stale, so users never get served a mismatched cache. You can verify locally any time with the same command (--checkreads only, never writes).
| Problem | Solution |
|---|---|
Tests fail with ReferenceError: document is not defined |
Ensure tests/setup.js is loaded — check jest.config or package.json setupFiles |
| Sandbox code execution hangs | Check SandboxRunner timeout config; the iframe srcdoc may have a CSP error — look at browser console |
| API key modal won't dismiss | Clear localStorage key openai_api_key and refresh |
| Service worker serves stale assets | Unregister SW in DevTools → Application → Service Workers, then hard refresh (Ctrl+Shift+R) |
npm test passes but browser shows errors |
jsdom and real browsers differ — test in Chrome DevTools console too |
Changes to style.css not visible |
SW cache — see Service Worker tip above, or use DevTools → Network → Disable cache |
We use a simple trunk-based flow:
main— stable, always deployable- Feature branches:
feature/<short-name>(e.g.,feature/voice-input) - Bug fixes:
fix/<issue-number>-<short-name>(e.g.,fix/42-sandbox-timeout) - Keep branches short-lived — merge within a few days, not weeks
- Rebase onto
mainbefore opening a PR to keep history linear
By contributing, you agree that your contributions will be licensed under the MIT License.