diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..d600b6c76dd93f7b2472160d42b2797cae50c8e5
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..a78447ebf932f1bb3a5b124b472bea8b3a86f80f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+[*]
+charset = utf-8
+insert_final_newline = true
+end_of_line = lf
+indent_style = space
+indent_size = 2
+max_line_length = 80
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..c46a790561a800e2cc153c51ebf8b857ce8918b0
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,27 @@
+# A comma-separated list of access keys. Example: `ACCESS_KEYS="ABC123,JUD71F,HUWE3"`. Leave blank for unrestricted access.
+ACCESS_KEYS=""
+
+# The timeout in hours for access key validation. Set to 0 to require validation on every page load.
+ACCESS_KEY_TIMEOUT_HOURS="24"
+
+# The default model ID for Wllama.
+WLLAMA_DEFAULT_MODEL_ID="qwen-3-0.6b"
+
+# The base URL for the internal OpenAI compatible API. Example: `INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL="https://api.openai.com/v1"`. Leave blank to disable internal OpenAI compatible API.
+INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL=""
+
+# The access key for the internal OpenAI compatible API.
+INTERNAL_OPENAI_COMPATIBLE_API_KEY=""
+
+# The model for the internal OpenAI compatible API.
+INTERNAL_OPENAI_COMPATIBLE_API_MODEL=""
+
+# The name of the internal OpenAI compatible API, displayed in the UI.
+INTERNAL_OPENAI_COMPATIBLE_API_NAME="Internal API"
+
+# The type of inference to use by default. The possible values are:
+# "browser" -> In the browser (Private)
+# "openai" -> Remote Server (API)
+# "horde" -> AI Horde (Pre-configured)
+# "internal" -> $INTERNAL_OPENAI_COMPATIBLE_API_NAME
+DEFAULT_INFERENCE_TYPE="browser"
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000000000000000000000000000000000..d8b74ecb7d9fc0bb8f7e0d0b1da260ff2938a903
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,84 @@
+# Contributor Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at:
+
+- GitHub: [@felladrin](https://github.com/felladrin)
+- Email: (private contact can be provided upon request)
+
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..47cc08519f9267c508d4c1501d2e8215ac14a7fb
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,50 @@
+# Contributing to MiniSearch
+
+First off, thank you for considering contributing to MiniSearch! It's people like you that make MiniSearch such a great tool.
+
+## Quick Start
+
+- **Onboarding**: See [`../docs/quick-start.md`](../docs/quick-start.md) for setup and running the app.
+- **Development workflow**: See [`../docs/pull-requests.md`](../docs/pull-requests.md) for the full PR process.
+- **Architecture guide**: See [`../agents.md`](../agents.md) for codebase navigation.
+- **Full docs**: Browse [`../docs/`](../docs/) for architecture, configuration, and AI integration.
+
+## Code Quality
+
+Before submitting a pull request, please run:
+
+```bash
+docker compose exec development-server npm run lint
+```
+
+This runs:
+- Biome (formatting/linting)
+- TypeScript (type checking)
+- knip (dead code detection)
+- jscpd (copy-paste detection)
+- Custom architectural linter
+- `doc-gardening.cjs` (documentation checks)
+- `documentation-validator.cjs` (documentation validation)
+
+## Reporting Bugs
+
+- Use the [GitHub issue tracker](https://github.com/felladrin/MiniSearch/issues) to report bugs.
+- Check if the issue has already been reported before creating a new one.
+- Provide clear steps to reproduce the issue.
+- Include your environment details (OS, browser, Docker version if applicable).
+- Add screenshots if the issue is UI-related.
+
+## Suggesting Features
+
+- Use the [GitHub issue tracker](https://github.com/felladrin/MiniSearch/issues) for feature suggestions.
+- Clearly describe the feature and why it would be useful.
+- Consider if it fits with the project's minimalist philosophy.
+- Provide examples of how you envision the feature working.
+
+## Questions?
+
+Don't hesitate to ask questions! We're here to help you contribute successfully.
+
+---
+
+Thanks again for your interest in contributing to MiniSearch! 🎉
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000000000000000000000000000000000..a537f6c1f52ff96ab8c44717fa72bcaa1fd05ddc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,123 @@
+---
+name: Bug report
+description: Create a report to help us improve
+title: "[BUG] "
+labels: ["bug"]
+assignees: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report! Please provide as much detail as possible.
+
+ - type: textarea
+ id: bug-description
+ attributes:
+ label: Describe the bug
+ description: A clear and concise description of what the bug is
+ placeholder: What happened? What did you expect to happen?
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: Steps to reproduce
+ description: Please provide detailed steps to reproduce the issue
+ placeholder: |
+ 1. Go to '...'
+ 2. Click on '....'
+ 3. Scroll down to '....'
+ 4. See error
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected-behavior
+ attributes:
+ label: Expected behavior
+ description: A clear and concise description of what you expected to happen
+ validations:
+ required: true
+
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots
+ description: If applicable, add screenshots to help explain your problem
+ placeholder: Drag and drop images here or paste them
+
+ - type: dropdown
+ id: os
+ attributes:
+ label: Operating System
+ description: What operating system are you using?
+ options:
+ - Windows 11
+ - Windows 10
+ - macOS 15.x
+ - macOS 14.x
+ - macOS 13.x
+ - Ubuntu 22.04
+ - Ubuntu 20.04
+ - Other Linux
+ - Other
+ validations:
+ required: true
+
+ - type: dropdown
+ id: browser
+ attributes:
+ label: Browser
+ description: What browser are you using?
+ options:
+ - Chrome
+ - Firefox
+ - Safari
+ - Edge
+ - Other
+ validations:
+ required: true
+
+ - type: input
+ id: version
+ attributes:
+ label: MiniSearch version
+ description: What version of MiniSearch are you using?
+ placeholder: latest, v1.0.0, etc.
+ validations:
+ required: true
+
+ - type: input
+ id: docker-version
+ attributes:
+ label: Docker version (if applicable)
+ description: What Docker version are you using?
+ placeholder: e.g., 24.0.7
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: Add any other context about the problem here
+
+ - type: dropdown
+ id: deployment-type
+ attributes:
+ label: Deployment type
+ description: How are you running MiniSearch?
+ options:
+ - Docker image
+ - Building from source
+ - Development server
+ - Other
+ validations:
+ required: true
+
+ - type: textarea
+ id: custom-config
+ attributes:
+ label: Custom configuration (if applicable)
+ description: Any custom configuration you're using
+ placeholder: Environment variables, custom settings, etc.
+---
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000000000000000000000000000000000..3c7173ac945df19485fd6743ecc1d20c76e64627
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,73 @@
+---
+name: Feature request
+description: Suggest an idea for this project
+title: "[FEATURE] "
+labels: ["enhancement"]
+assignees: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for suggesting a new feature! Please provide as much detail as possible to help us understand your vision.
+
+ - type: textarea
+ id: problem-description
+ attributes:
+ label: Is your feature request related to a problem?
+ description: A clear and concise description of what the problem is
+ placeholder: I'm always frustrated when [...]
+ validations:
+ required: true
+
+ - type: textarea
+ id: solution-description
+ attributes:
+ label: Describe the solution you'd like
+ description: A clear and concise description of what you want to happen
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Describe alternatives you've considered
+ description: A clear and concise description of any alternative solutions or features you've considered
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: Add any other context or screenshots about the feature request here
+
+ - type: dropdown
+ id: alignment
+ attributes:
+ label: Alignment with MiniSearch's goals
+ description: How does this feature align with MiniSearch's minimalist, privacy-focused philosophy?
+ options:
+ - "Strongly aligns - enhances privacy/minimalism"
+ - "Moderately aligns - useful but adds complexity"
+ - "Needs consideration - potential trade-offs"
+ - "Not sure - need discussion"
+ validations:
+ required: true
+
+ - type: textarea
+ id: implementation-ideas
+ attributes:
+ label: Implementation ideas (optional)
+ description: Do you have any ideas about how this could be implemented?
+ placeholder: Technical thoughts, potential approaches, etc.
+
+ - type: dropdown
+ id: priority
+ attributes:
+ label: Priority (your assessment)
+ description: How important is this feature to you?
+ options:
+ - "High - would significantly improve my experience"
+ - "Medium - nice to have"
+ - "Low - minor improvement"
+ validations:
+ required: true
+---
diff --git a/.github/ISSUE_TEMPLATE/security_vulnerability.md b/.github/ISSUE_TEMPLATE/security_vulnerability.md
new file mode 100644
index 0000000000000000000000000000000000000000..e464b0b43e7e8b09b78344df959e01ad3b49fa00
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/security_vulnerability.md
@@ -0,0 +1,98 @@
+---
+name: Security Vulnerability
+description: Report a security vulnerability (PRIVATE - will be converted to private report)
+title: "[SECURITY] "
+labels: ["security"]
+assignees: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ # ⚠️ IMPORTANT: SECURITY VULNERABILITIES SHOULD NOT BE REPORTED PUBLICLY ⚠️
+
+ This template will create a public issue, but for security vulnerabilities, please use one of these private reporting methods:
+
+ 1. **Preferred**: Use GitHub's [Private Vulnerability Reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
+ 2. **Alternative**: Email the maintainer privately
+
+ If you accidentally created a public issue, please close it and report privately instead.
+
+ - type: checkboxes
+ id: confirmation
+ attributes:
+ label: Confirmation
+ description: Please confirm you understand the security reporting process
+ options:
+ - label: I understand this should be reported privately and will use the methods above
+ required: true
+
+ - type: dropdown
+ id: vulnerability-type
+ attributes:
+ label: Vulnerability Type
+ description: What type of security vulnerability is this?
+ options:
+ - Cross-site Scripting (XSS)
+ - Authentication/Authorization Bypass
+ - Information Disclosure
+ - Remote Code Execution
+ - Denial of Service
+ - Configuration Issue
+ - Other
+ validations:
+ required: true
+
+ - type: dropdown
+ id: severity
+ attributes:
+ label: Severity
+ description: How severe is this vulnerability?
+ options:
+ - Critical
+ - High
+ - Medium
+ - Low
+ validations:
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: A clear description of the security vulnerability
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: Steps to Reproduce
+ description: Detailed steps to reproduce the vulnerability
+ validations:
+ required: true
+
+ - type: textarea
+ id: impact
+ attributes:
+ label: Impact
+ description: Describe the potential impact of this vulnerability
+ validations:
+ required: true
+
+ - type: textarea
+ id: mitigation
+ attributes:
+ label: Mitigation (if known)
+ description: Any known workarounds or mitigations
+
+ - type: textarea
+ id: additional-information
+ attributes:
+ label: Additional Information
+ description: Any additional context or information about the vulnerability
+
+ - type: markdown
+ attributes:
+ value: |
+ ## 🚨 PLEASE DO NOT SUBMIT THIS PUBLICLY - USE PRIVATE REPORTING METHODS ABOVE 🚨
+---
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000000000000000000000000000000000..8f117281de83814ea7d18bc6e73ff1173fa735c4
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,21 @@
+## Description
+
+
+## Type of Change
+- [ ] Bug fix
+- [ ] New feature
+- [ ] Documentation
+- [ ] Other (refactor, build, chore)
+
+## How to test
+
+
+## Checklist
+- [ ] `npm run lint` passes
+- [ ] Tests pass (`npm run test`), with tests added where it made sense
+
+## Screenshots
+
+
+## Security, performance, or breaking changes
+
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 0000000000000000000000000000000000000000..9527ba2700b030389dd853738e73611949a39d4f
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,135 @@
+# Security Policy
+
+## Supported Versions
+
+Only the latest version of MiniSearch receives security updates.
+
+| Version | Supported |
+|---------|------------|
+| Latest | ✅ |
+| Older | ❌ |
+
+## Reporting a Vulnerability
+
+### Private Vulnerability Reporting
+
+We strongly encourage using GitHub's [Private Vulnerability Reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) feature to report security vulnerabilities.
+
+**Do not report security vulnerabilities through public issues.**
+
+### How to Report
+
+1. **Preferred**: Use GitHub's Private Vulnerability Reporting
+2. **Alternative**: Email the maintainer privately at: (contact can be provided upon request)
+
+When reporting a vulnerability, please include:
+- A clear description of the vulnerability
+- Steps to reproduce the issue
+- Potential impact of the vulnerability
+- Any suggested mitigations (if known)
+
+### What to Expect
+
+- We will acknowledge receipt of your report within 48 hours
+- We will provide a detailed response within 7 days
+- We will work with you to understand and validate the report
+- We will coordinate disclosure timing to minimize user risk
+
+## Security Scope
+
+### In Scope
+
+- Vulnerabilities in the MiniSearch web application
+- Security issues in the Docker container configuration
+- Authentication and authorization bypasses
+- Cross-site scripting (XSS) vulnerabilities
+- Information disclosure issues
+- Remote code execution vulnerabilities
+- Privilege escalation in the application context
+
+### Out of Scope
+
+- Issues in third-party dependencies (report to respective projects)
+- Vulnerabilities in the underlying browser or Node.js runtime
+- Physical attacks on infrastructure
+- Social engineering attacks
+- Denial of service attacks that don't indicate a vulnerability
+- Issues requiring physical access to user devices
+
+## Threat Model
+
+### MiniSearch's Security Boundaries
+
+MiniSearch is designed as a privacy-focused search application with the following security assumptions:
+
+**Trust Boundaries:**
+- **Browser Environment**: The application runs entirely in the user's browser
+- **Server Component**: Optional backend for search and AI processing
+- **AI Models**: Local or remote AI processing with configurable endpoints
+
+**Data Flow:**
+- User queries are sent to SearXNG instances (configurable)
+- AI processing can be local (Wllama) or remote (API endpoints)
+- Search history is stored locally in the browser
+- No tracking or analytics by default
+
+**Security Controls:**
+- Optional access key protection for deployment
+- Configurable AI endpoints for privacy
+- Local-first data storage
+- No third-party tracking or analytics
+
+**Potential Risks:**
+- Malicious SearXNG instances could log queries
+- Remote AI endpoints could access user queries
+- Browser extensions could interfere with the application
+- Man-in-the-middle attacks without HTTPS
+
+## Security Best Practices
+
+### For Users
+
+- Always use HTTPS when accessing MiniSearch instances
+- Configure trusted SearXNG instances
+- Use local AI models for maximum privacy
+- Set access keys for deployed instances
+- Keep browsers updated
+
+### For Deployers
+
+- Use the official Docker image
+- Configure environment variables securely
+- Set up proper access controls
+- Use HTTPS in production
+- Regularly update dependencies
+- Monitor for security advisories
+
+## Security Features
+
+- **Access Key Protection**: Optional password-based access control
+- **Configurable Endpoints**: Users control search and AI providers
+- **Local Processing**: AI models can run entirely in the browser
+- **No Tracking**: Built without analytics or tracking
+- **HTTPS Ready**: Designed for secure deployment
+
+## Security Updates
+
+Security updates will be:
+- Released as new versions
+- Announced in release notes
+- Coordinated with dependency updates when applicable
+
+## Security Team
+
+The MiniSearch security team is currently the project maintainer:
+- [@felladrin](https://github.com/felladrin) - Project Maintainer
+
+## Acknowledgments
+
+We thank security researchers who help us keep MiniSearch secure. All valid security reports will be acknowledged in our release notes (with reporter permission).
+
+## Related Resources
+
+- [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories)
+- [OWASP Web Security Testing Guide](https://owasp.org/www-project-web-security-testing-guide/)
+- [Mozilla Security Guidelines](https://infosec.mozilla.org/guidelines)
diff --git a/.github/hf-space-config.yml b/.github/hf-space-config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6d9d5b00160bb6ad09b8a92ab74ab6cd0a360b3b
--- /dev/null
+++ b/.github/hf-space-config.yml
@@ -0,0 +1,11 @@
+title: MiniSearch
+emoji: 👌🔍
+colorFrom: yellow
+colorTo: yellow
+sdk: docker
+short_description: Minimalist web-searching app with browser-based AI assistant
+pinned: true
+custom_headers:
+ cross-origin-embedder-policy: require-corp
+ cross-origin-opener-policy: same-origin
+ cross-origin-resource-policy: cross-origin
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e185d34eb231df099ee546e79b38414d8b51b814
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,39 @@
+name: CI
+
+on:
+ push:
+ branches: [main, master]
+ pull_request:
+ branches: [main, master]
+
+jobs:
+ build-test:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ strategy:
+ matrix:
+ node-version: [lts/*, current]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run lint
+ run: npm run lint
+
+ - name: Check formatting
+ run: npm run format:check
+
+ - name: Run tests
+ run: npm test
diff --git a/.github/workflows/deploy-to-hugging-face.yml b/.github/workflows/deploy-to-hugging-face.yml
new file mode 100644
index 0000000000000000000000000000000000000000..12688c4d5400ad9ed28f3ae958db126e8c3a3b52
--- /dev/null
+++ b/.github/workflows/deploy-to-hugging-face.yml
@@ -0,0 +1,24 @@
+name: Deploy to Hugging Face
+
+on:
+ workflow_dispatch:
+
+jobs:
+ sync-to-hf:
+ name: Sync to Hugging Face Spaces
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v7
+ # Pin to the tip of the action's `v1` branch, which is where its build
+ # output lives. Its release tags point at source commits, and `dist/` is
+ # gitignored there, so a tag digest resolves to a tree with no
+ # `dist/index.js` and the step fails before it runs.
+ - uses: JacobLinCool/huggingface-sync@48eb49940149dc1015d8294621cb203635fe9ff3 # v1 branch, 2026-05-12
+ with:
+ github: ${{ secrets.GITHUB_TOKEN }}
+ user: ${{ vars.HF_SPACE_OWNER }}
+ space: ${{ vars.HF_SPACE_NAME }}
+ token: ${{ secrets.HF_TOKEN }}
+ configuration: ".github/hf-space-config.yml"
diff --git a/.github/workflows/on-pull-request-to-main.yml b/.github/workflows/on-pull-request-to-main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7976fd1438eb13ca09681a420eaa70f2792bf569
--- /dev/null
+++ b/.github/workflows/on-pull-request-to-main.yml
@@ -0,0 +1,11 @@
+name: On Pull Request To Main
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ branches: ["main"]
+jobs:
+ check-docker:
+ if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-check-docker') }}
+ permissions:
+ contents: read
+ uses: ./.github/workflows/reusable-check-docker.yml
diff --git a/.github/workflows/on-push-to-main.yml b/.github/workflows/on-push-to-main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..43350e46e1f4237d8b1a6bc5adeadb7473460ec3
--- /dev/null
+++ b/.github/workflows/on-push-to-main.yml
@@ -0,0 +1,9 @@
+name: On Push To Main
+on:
+ push:
+ branches: ["main"]
+jobs:
+ check-docker:
+ permissions:
+ contents: read
+ uses: ./.github/workflows/reusable-check-docker.yml
diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml
new file mode 100644
index 0000000000000000000000000000000000000000..185c5dfd137b8c63bbcfc641b69e606b817e3e1d
--- /dev/null
+++ b/.github/workflows/publish-docker-image.yml
@@ -0,0 +1,39 @@
+name: Publish Docker Image
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build-and-push-image:
+ name: Publish Docker Image to GitHub Packages
+ runs-on: ubuntu-latest
+ env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+ - name: Log in to the Container registry
+ uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
+ - name: Build and push Docker Image
+ uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/reusable-check-docker.yml b/.github/workflows/reusable-check-docker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8a4e826dbc28fc344253a0404f0c60a1e4dfa0d2
--- /dev/null
+++ b/.github/workflows/reusable-check-docker.yml
@@ -0,0 +1,15 @@
+on:
+ workflow_call:
+jobs:
+ check-docker-container:
+ name: Check Docker Container
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v7
+ - run: docker compose -f docker-compose.production.yml up -d
+ - name: Check if main page is available
+ run: until curl -s -o /dev/null -w "%{http_code}" localhost:7860 | grep 200; do sleep 1; done
+ timeout-minutes: 1
+ - run: docker compose -f docker-compose.production.yml down
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..5c56e346682ab98b5bc4cf6aadb4e2df17fe294a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+/client/dist
+/server/models
+.vscode
+/vite-build-stats.html
+.env
+/coverage
+.playwright-cli
+*.tsbuildinfo
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000000000000000000000000000000000000..a2e58b165995d7241de75e6805ca8a0d6932fe82
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+npm install --no-save @biomejs/biome && npx @biomejs/biome check --write --staged --no-errors-on-unmatched && git diff --name-only --cached | while read -r file; do [ -f "$file" ] && git add "$file"; done
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000000000000000000000000000000000..80bcbed90c4f2b3d895d5086dc775e1bd8b32b43
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+legacy-peer-deps = true
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000000000000000000000000000000000000..b009dfb9d9f98f39d71d2a4ecc3110332b969fd3
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+lts/*
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..52e3f153b7acc728ee4a1ffb6472858a5a6ba9be
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,78 @@
+FROM node:lts
+
+ARG SEARXNG_COMMIT_SHA="6da6eee265daeb4a62ab638d6921522bf405de69"
+
+ENV PORT=7860
+EXPOSE $PORT
+
+ARG USERNAME=node
+ARG HOME_DIR=/home/${USERNAME}
+ARG APP_DIR=${HOME_DIR}/app
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ python3 \
+ python3-venv && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+RUN mkdir -p /usr/local/searxng /etc/searxng && \
+ chown -R ${USERNAME}:${USERNAME} /usr/local/searxng /etc/searxng && \
+ chmod 755 /etc/searxng
+
+WORKDIR /usr/local/searxng
+RUN python3 -m venv searxng-venv && \
+ chown -R ${USERNAME}:${USERNAME} /usr/local/searxng/searxng-venv && \
+ /usr/local/searxng/searxng-venv/bin/pip install --upgrade pip && \
+ /usr/local/searxng/searxng-venv/bin/pip install wheel setuptools pyyaml lxml
+
+RUN git clone https://github.com/searxng/searxng.git /usr/local/searxng/searxng-src && \
+ git -C /usr/local/searxng/searxng-src checkout $SEARXNG_COMMIT_SHA && \
+ chown -R ${USERNAME}:${USERNAME} /usr/local/searxng/searxng-src
+
+ARG SEARXNG_SETTINGS_PATH="/etc/searxng/settings.yml"
+
+COPY --chown=${USERNAME}:${USERNAME} searxng-settings.yml $SEARXNG_SETTINGS_PATH
+
+WORKDIR /usr/local/searxng/searxng-src
+RUN chmod 644 $SEARXNG_SETTINGS_PATH && \
+ sed -i 's/ultrasecretkey/'$(openssl rand -hex 32)'/g' $SEARXNG_SETTINGS_PATH && \
+ /usr/local/searxng/searxng-venv/bin/pip install -r requirements.txt && \
+ /usr/local/searxng/searxng-venv/bin/pip install --no-build-isolation -e .
+
+USER ${USERNAME}
+
+WORKDIR ${APP_DIR}
+
+ARG ACCESS_KEYS
+ARG ACCESS_KEY_TIMEOUT_HOURS
+ARG WLLAMA_DEFAULT_MODEL_ID
+ARG INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL
+ARG INTERNAL_OPENAI_COMPATIBLE_API_KEY
+ARG INTERNAL_OPENAI_COMPATIBLE_API_MODEL
+ARG INTERNAL_OPENAI_COMPATIBLE_API_NAME
+ARG DEFAULT_INFERENCE_TYPE
+ARG HOST
+ARG HMR_PORT
+ARG ALLOWED_HOSTS
+
+COPY --chown=${USERNAME}:${USERNAME} ./package.json ./package-lock.json ./.npmrc ./
+
+RUN npm ci
+
+COPY --chown=${USERNAME}:${USERNAME} . .
+
+# The commit hash is optional build metadata, so a build context without a
+# usable repository must not fail the build. This happens when building from a
+# git worktree, where `.git` is a file pointing at a gitdir outside the context;
+# git then treats every command as fatal, including `config --global`.
+RUN git config --global --add safe.directory ${APP_DIR} 2>/dev/null || true; \
+ git rev-parse --short HEAD >/dev/null 2>&1 || \
+ echo "WARNING: no usable git repository in the build context, so the app will report an empty commit hash."; \
+ npm run build
+
+HEALTHCHECK --interval=5m CMD curl -f http://localhost:7860/status || exit 1
+
+ENTRYPOINT [ "/bin/sh", "-c" ]
+
+CMD ["(cd /usr/local/searxng/searxng-src && /usr/local/searxng/searxng-venv/bin/python -m searx.webapp > /dev/null 2>&1) & npm start -- --host"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..904736f0d2f11cd79d21a39ebe7babef2840fb71
--- /dev/null
+++ b/README.md
@@ -0,0 +1,196 @@
+---
+title: MiniSearch
+emoji: 👌🔍
+colorFrom: yellow
+colorTo: yellow
+sdk: docker
+short_description: Minimalist web-searching app with browser-based AI assistant
+pinned: true
+custom_headers:
+ cross-origin-embedder-policy: require-corp
+ cross-origin-opener-policy: same-origin
+ cross-origin-resource-policy: cross-origin
+---
+
+
+
+## About
+
+MiniSearch is a self-hosted search engine with an AI assistant.
+
+The AI can run entirely inside your browser tab, on GPU or CPU, so a working setup needs no API key, no separate inference server, and no third party seeing your queries. Web results come from a bundled [SearXNG](https://github.com/searxng/searxng) metasearch instance, are reranked locally, and the whole thing ships as a single Docker container.
+
+## Features
+
+- **Private by design.** No tracking, no telemetry, no accounts. Search history, cached results, and chats are stored in your browser and never leave your machine.
+- **AI in your browser.** Pick from 30+ curated models (135M to 4B parameters) that run on WebGPU where available and on CPU elsewhere. Models are downloaded once and cached by the browser.
+- **Any backend you like.** Connect an OpenAI-compatible API (Ollama, LM Studio, vLLM, llama.cpp server, or a hosted provider), use the crowdsourced AI Horde, or let the server proxy your own API without exposing its key.
+- **A real search pipeline.** Text and image results aggregated by SearXNG, reranked locally by a cross-encoder model, cached, and rate-limited; all inside the container.
+- **Answers you can verify.** Responses cite the sources they draw from, support follow-up questions with conversation memory, reveal the model's reasoning on demand, and can be read aloud.
+- **Local history and analytics.** Fuzzy-searchable history with pinning and full-session restore, plus usage statistics and an activity heatmap. Retention is configurable, and storage stays in the browser.
+- **Fits your workflow.** Set it as your browser's default search engine, trigger it from Raycast, embed it in your own pages, and optionally protect your instance with access keys.
+
+## Quick start
+
+Run the published image:
+
+```bash
+docker run -p 7860:7860 ghcr.io/felladrin/minisearch:main
+```
+
+Then open and start searching.
+
+
+Use Docker Compose
+
+Add the service to your `docker-compose.yml`:
+
+```yaml
+services:
+ minisearch:
+ image: ghcr.io/felladrin/minisearch:main
+ ports:
+ - "7860:7860"
+```
+
+
+
+
+Build from source
+
+```bash
+git clone https://github.com/felladrin/MiniSearch.git
+cd MiniSearch
+docker compose -f docker-compose.production.yml up --build
+```
+
+
+
+
+Host it on Hugging Face
+
+[Duplicate the Space](https://huggingface.co/spaces/Felladrin/MiniSearch?duplicate=true) to get your own hosted instance, no server required. Environment variables can be set in the Space settings.
+
+
+
+## How it works
+
+```mermaid
+flowchart LR
+ subgraph browser [Your browser]
+ UI[Search UI]
+ LocalAI[In-browser model WebGPU or CPU]
+ Storage[(IndexedDB history and cache)]
+ end
+ subgraph container [Docker container]
+ Server[App server]
+ SearXNG[SearXNG metasearch]
+ Reranker[Reranker ONNX Runtime]
+ end
+ UI <--> Storage
+ UI -->|search| Server
+ Server --> SearXNG
+ SearXNG --> Web((Web))
+ Server --> Reranker
+ UI -.->|in-browser inference| LocalAI
+ UI -.->|remote inference, optional| RemoteAPI[OpenAI-compatible API or AI Horde]
+```
+
+Your query goes to the app server, which asks the bundled SearXNG instance to aggregate results from multiple search engines. The server reranks them with a small cross-encoder model before returning them, and the browser caches them locally. If the AI response is enabled, the assistant reads the top results and writes a cited answer, either with a model running in your browser or through the backend you configured. The full picture is in [docs/overview.md](docs/overview.md).
+
+## Configuration
+
+Set these in a `.env` file (see [.env.example](.env.example)) when using Docker Compose, or pass them with `-e`/`--env-file` to `docker run`:
+
+| Variable | Purpose | Default |
+| --- | --- | --- |
+| `ACCESS_KEYS` | Comma-separated keys that gate access to the instance | Unset (open access) |
+| `ACCESS_KEY_TIMEOUT_HOURS` | How long a validated key stays cached in the browser | `24` |
+| `WLLAMA_DEFAULT_MODEL_ID` | Default model for in-browser inference | `qwen-3-0.6b` |
+| `DEFAULT_INFERENCE_TYPE` | Backend preselected in the UI: `browser`, `openai`, `horde`, or `internal` | `browser` |
+| `INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL` | Base URL of a self-hosted API that the server proxies for its users | Unset (disabled) |
+| `INTERNAL_OPENAI_COMPATIBLE_API_KEY` | Key for that API; never sent to clients | Unset |
+| `INTERNAL_OPENAI_COMPATIBLE_API_MODEL` | Model served through that API | Auto-detected |
+| `INTERNAL_OPENAI_COMPATIBLE_API_NAME` | Name shown for it in the UI | `Internal API` |
+
+The full reference, including server options such as `PORT` and `ALLOWED_HOSTS`, is in [docs/configuration.md](docs/configuration.md).
+
+## FAQ
+
+
+How do I make it my browser's default search engine?
+
+Add a custom search engine in your browser settings using the pattern `http://localhost:7860/?q=%s`, replacing the host with your instance's address. Your search term replaces `%s`.
+
+
+
+
+How do I search from Raycast?
+
+Add [this Quicklink](https://ray.so/quicklinks/shared?quicklinks=%7B%22link%22:%22https:%5C/%5C/felladrin-minisearch.hf.space%5C/?q%3D%7BQuery%7D%22,%22name%22:%22MiniSearch%22%7D) to Raycast, and edit it to point to your own instance if you have one.
+
+
+
+
+Can I use my own models through an OpenAI-compatible API?
+
+Yes. Open the menu, set "AI Processing Location" to `Remote server (API)`, then fill in the base URL, and optionally an API key and a model name. If the model is left blank, it is picked from the ones the API lists.
+
+
+
+
+Can others use my instance with my API key without seeing it?
+
+Yes. Configure the `INTERNAL_OPENAI_COMPATIBLE_API_*` variables from the [Configuration](#configuration) table and restart the container. A new option with the name you chose appears in the "AI Processing Location" menu, and the key stays on the server.
+
+
+
+## Contributing
+
+Contributions are welcome. To set up a development environment:
+
+```bash
+git clone https://github.com/felladrin/MiniSearch.git
+cd MiniSearch
+docker compose up
+```
+
+The development server runs at . Hot Module Replacement (HMR) is available on . Before opening a pull request, run the quality gate:
+
+```bash
+docker compose exec development-server npm run lint
+```
+
+See the [Contributing Guidelines](.github/CONTRIBUTING.md), [Code of Conduct](.github/CODE_OF_CONDUCT.md), and [Security Policy](.github/SECURITY.md). The codebase is documented for humans and AI agents alike: [agents.md](agents.md) is the navigation hub, and [docs/](docs/) covers the architecture in depth.
+
+## Acknowledgments
+
+MiniSearch builds on the work of these projects:
+
+| Project | Role |
+| --- | --- |
+| [SearXNG](https://github.com/searxng/searxng) | Metasearch engine behind the results |
+| [ONNX Runtime](https://github.com/microsoft/onnxruntime) | Local reranking of the search results |
+| [wllama](https://github.com/ngxson/wllama) and [llama.cpp](https://github.com/ggml-org/llama.cpp) | In-browser inference |
+| [AI Horde](https://aihorde.net) | Crowdsourced distributed inference |
+| [AI SDK](https://github.com/vercel/ai) | Client for OpenAI-compatible APIs |
+| [Mantine](https://mantine.dev) | UI components |
+| [Hugging Face](https://huggingface.co) | Model hosting and the live demo Space |
+
+## License
+
+MiniSearch is released under the [Apache License 2.0](license.txt).
diff --git a/agents.md b/agents.md
new file mode 100644
index 0000000000000000000000000000000000000000..165be3b58b7a9efaf30b1ccc5dcc32f1bbc3b574
--- /dev/null
+++ b/agents.md
@@ -0,0 +1,224 @@
+# MiniSearch Agent Guidelines
+
+This is your navigation hub. Start here, follow the links, and return when you need orientation.
+
+## Before You Start
+
+**New to this codebase?** Read in this order:
+1. `docs/quick-start.md` - Get it running
+2. `docs/overview.md` - Understand the system
+3. `docs/project-structure.md` - Navigate the code
+
+**Making changes?** Check:
+- `docs/coding-conventions.md` - Code style
+- `docs/development-commands.md` - Available commands
+- `docs/pull-requests.md` - How to submit
+
+## Repository Map
+
+### Getting Started
+- **`docs/quick-start.md`** - Installation, first run, verification
+- **`docs/overview.md`** - System architecture and data flow
+- **`docs/project-structure.md`** - Directory layout and component organization
+
+### Configuration & Setup
+- **`docs/configuration.md`** - Environment variables and settings reference
+- **`docs/security.md`** - Access control, privacy, and security model
+
+### Core Functionality
+- **`docs/ai-integration.md`** - AI inference types (Wllama, OpenAI, AI Horde, Internal)
+- **`docs/ui-components.md`** - Component architecture and PubSub patterns
+- **`docs/search-history.md`** - History database schema and management
+- **`docs/conversation-memory.md`** - Token budgeting and rolling summaries
+- **`docs/reranking.md`** - Reranker subsystem and model lifecycle
+- **`docs/glossary.md`** - Codebase-specific terms and domain concepts
+
+### Development
+- **`docs/development-commands.md`** - Docker, npm, and testing commands
+- **`docs/coding-conventions.md`** - Style guide and patterns
+- **`docs/pull-requests.md`** - PR process and merge philosophy
+- **`docs/core-technologies.md`** - Technology stack and dependencies
+- **`docs/design.md`** - UI/UX design principles
+
+## Agent Decision Tree
+
+```
+Need to:
+├── Add a feature?
+│ ├── UI component → docs/ui-components.md
+│ ├── AI integration → docs/ai-integration.md
+│ ├── Search functionality → client/modules/search.ts
+│ └── Settings option → docs/configuration.md
+├── Fix a bug?
+│ ├── UI issue → Check component + PubSub channels
+│ ├── AI not working → docs/ai-integration.md + browser console
+│ ├── Search failing → Check SearXNG + server hooks
+│ └── Build error → docs/development-commands.md
+├── Configure deployment?
+│ ├── Environment variables → docs/configuration.md
+│ ├── Access control → docs/security.md
+│ └── Docker setup → docs/overview.md
+└── Understand data flow?
+ ├── Search flow → client/modules/search.ts
+ ├── AI generation → client/modules/textGeneration.ts
+ ├── State management → docs/ui-components.md
+ └── History/Chat → docs/search-history.md + docs/conversation-memory.md
+```
+
+## Key Files Reference
+
+### Entry Points
+- `client/index.tsx` - React app initialization
+- `vite.config.ts` - Vite dev server with hooks
+- `Dockerfile` - Multi-stage container build
+
+### Business Logic Modules
+- `client/modules/search.ts` - Search orchestration and caching
+- `client/modules/textGeneration.ts` - AI response flow
+- `client/modules/pubSub.ts` - All PubSub channels
+- `client/modules/settings.ts` - Settings management
+- `client/modules/history.ts` - Search history database
+
+### Server-Side Modules
+- `server/searchEndpointServerHook.ts` - `/search` endpoints
+- `server/internalApiEndpointServerHook.ts` - `/inference` proxy
+- `server/webSearchService.ts` - SearXNG integration
+- `server/rerankerService.ts` - Local result reranking
+
+### UI Components
+- `client/components/App/` - Application shell with error boundaries
+- `client/components/Search/Form/` - Search input and form
+- `client/components/Search/Results/` - Textual and graphical results display
+- `client/components/Search/History/` - History drawer and button
+- `client/components/AiResponse/` - AI response display and chat interface
+- `client/components/Pages/Main/` - Main page layout
+- `client/components/Pages/Main/Menu/` - Settings drawers (AI, Search, Interface, History, Voice, Actions)
+- `client/components/Pages/AccessPage.tsx` - Access key validation page
+- `client/components/Analytics/SearchStats.tsx` - Search analytics cards
+- `client/components/Logs/` - Application logging modal
+- `client/components/Settings/HistorySettings.tsx` - History configuration UI
+
+### Client Modules
+- `client/modules/pubSub.ts` - All PubSub channels for state management
+- `client/modules/search.ts` - Search orchestration and IndexedDB caching
+- `client/modules/textGeneration.ts` - AI response generation and chat handling
+- `client/modules/textGenerationWithWllama.ts` - Browser-based inference
+- `client/modules/textGenerationWithOpenAi.ts` - OpenAI-compatible API inference
+- `client/modules/textGenerationWithHorde.ts` - AI Horde distributed inference
+- `client/modules/textGenerationWithInternalApi.ts` - Internal API proxy inference
+- `client/modules/settings.ts` - Default settings and inference type definitions
+- `client/modules/history.ts` - IndexedDB persistence for searches and chat
+- `client/modules/wllama.ts` - Wllama model configuration and management
+- `client/modules/webGpu.ts` - WebGPU availability detection (`"gpu" in navigator`)
+- `client/modules/querySuggestions.ts` - Search suggestion UI, stored in IndexedDB
+- `client/modules/relatedSearchQuery.ts` - Generates related search queries
+- `client/modules/followUpQuestions.ts` - Generates follow-up questions via `followUpQuestionPubSub`
+- `client/modules/accessKey.ts` - Validates and stores access keys using argon2id hashing and localStorage
+- `client/modules/parentWindow.ts` - PostMessage API for embedding in parent windows
+- `client/modules/searchTokenHash.ts` - CSRF protection token generation
+- `client/modules/systemPrompt.ts` - System prompt templates
+- `client/modules/logEntries.ts` - Application logging with unique IDs
+- `client/modules/appInfo.ts` - Application metadata and version info
+- `client/modules/keyboard.ts` - Keyboard shortcut handling
+- `client/modules/stringFormatters.ts` - Text formatting utilities
+- `client/modules/types.ts` - Shared TypeScript type definitions
+
+### Server Modules
+- `server/searchEndpointServerHook.ts` - `/search/text` and `/search/images` endpoints
+- `server/internalApiEndpointServerHook.ts` - `/inference` proxy to self-hosted API
+- `server/validateAccessKeyServerHook.ts` - Access key validation endpoint
+- `server/statusEndpointServerHook.ts` - `/status` health check endpoint
+- `server/rerankerServiceHook.ts` - Reranker model lifecycle management
+- `server/compressionServerHook.ts` - gzip/brotli compression for responses
+- `server/crossOriginServerHook.ts` - COOP/COEP headers for SharedArrayBuffer
+- `server/cacheServerHook.ts` - Cache-Control headers (preview server only)
+- `server/webSearchService.ts` - SearXNG integration with circuit breaker and retry logic
+- `server/rerankerService.ts` - Reranker service (ONNX Runtime inference)
+- `server/rankSearchResults.ts` - Score-based filtering and result reordering
+- `server/searchToken.ts` - CSRF token generation and storage
+- `server/verifiedTokens.ts` - In-memory `Set` of verified session tokens
+- `server/verifyTokenAndRateLimit.ts` - Token verification and rate limiting
+- `server/handleTokenVerification.ts` - Search token validation logic
+- `server/searchesSinceLastRestart.ts` - In-memory search counters for analytics
+- `server/downloadFileFromHuggingFaceRepository.ts` - Downloads model files from HuggingFace
+
+### Hooks
+- `client/hooks/useSearchHistory.ts` - Search history management from IndexedDB
+- `client/hooks/useHistoryRestore.ts` - Restores full search state from history
+- `client/hooks/useDrawerState.ts` - Drawer open/close state with logging
+
+## Common Tasks Quick Reference
+
+### Add a new AI model
+1. Add to `client/modules/wllama.ts`
+2. Update `docs/ai-integration.md`
+3. Update `docs/configuration.md` defaults
+
+### Add a new setting
+1. Add to `client/modules/settings.ts` default object
+2. Add UI in `client/components/Pages/Main/Menu/`
+3. Update `docs/configuration.md` settings table
+
+### Modify search behavior
+1. Edit `client/modules/search.ts`
+2. Update `server/webSearchService.ts` if server-side changes needed
+3. Check `server/rerankerService.ts` if reranking affected
+
+### Fix UI state issues
+1. Check PubSub channels in `client/modules/pubSub.ts`
+2. Verify component subscriptions in `docs/ui-components.md`
+3. Ensure proper state updates in business logic modules
+
+### Analyze test coverage
+1. Run `npm run test:coverage` to generate reports
+2. Check `coverage/coverage-summary.json` for quick metrics
+3. See `docs/development-commands.md` for full coverage analysis guide
+
+## Quality Gates
+
+Before any change:
+```bash
+docker compose exec development-server npm run lint
+```
+
+This runs:
+- Biome (formatting/linting)
+- TypeScript (type checking)
+- knip (dead code detection)
+- jscpd (copy-paste detection)
+- Custom architectural linter
+- Doc gardening and documentation validator scripts
+
+## Agent-First Principles
+
+**Repository as System of Record:**
+- All knowledge lives in versioned docs/ structure
+- This file is your entry point - start here
+- Follow links, don't assume - verify in code
+
+**Context Efficiency:**
+- Use this map to navigate quickly
+- Return to this file when context drifts
+- Follow the decision tree for common tasks
+
+**Architecture & Boundaries:**
+- Respect PubSub boundaries - don't cross concerns
+- Client vs server - keep them separate
+- Feature-based organization - one folder per feature
+
+**Documentation Maintenance:**
+- Update these docs when you learn something new
+- Add cross-references when linking concepts
+- Keep examples current with actual code
+
+## Technology Stack
+
+React + TypeScript + Mantine UI v9, with privacy-first architecture.
+See `docs/core-technologies.md` for complete dependency list and selection criteria.
+
+## Need Help?
+
+1. Check relevant doc in `docs/`
+2. Read the module code in `client/modules/` or `server/`
+3. Look at similar existing implementations
+4. Run `npm run lint` to validate changes
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000000000000000000000000000000000000..4891c3d4b160a71052aab2c7e1166287dd319301
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.5.2/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "ignoreUnknown": false
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space"
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "preset": "recommended"
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double"
+ }
+ },
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ }
+}
diff --git a/client/components/AiResponse/AiModelDownloadAllowanceContent.tsx b/client/components/AiResponse/AiModelDownloadAllowanceContent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..48fbb9ce81045bc0cd3ba2e5f99b66cfb7e30c5c
--- /dev/null
+++ b/client/components/AiResponse/AiModelDownloadAllowanceContent.tsx
@@ -0,0 +1,62 @@
+import { Alert, Button, Group, Text } from "@mantine/core";
+import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react";
+import { usePubSub } from "create-pubsub/react";
+import { useState } from "react";
+import { addLogEntry } from "@/modules/logEntries";
+import { settingsPubSub } from "@/modules/pubSub";
+
+export default function AiModelDownloadAllowanceContent() {
+ const [settings, setSettings] = usePubSub(settingsPubSub);
+ const [hasDeniedDownload, setDeniedDownload] = useState(false);
+
+ const handleAccept = () => {
+ setSettings({
+ ...settings,
+ allowAiModelDownload: true,
+ });
+ addLogEntry("User allowed the AI model download");
+ };
+
+ const handleDecline = () => {
+ setDeniedDownload(true);
+ addLogEntry("User denied the AI model download");
+ };
+
+ return hasDeniedDownload ? null : (
+ }
+ >
+
+ To obtain AI responses, a language model needs to be downloaded to your
+ browser. Enabling this option lets the app store it and load it
+ instantly on subsequent uses.
+
+
+ Please note that the download size ranges from 100 MB to 4 GB, depending
+ on the model you select in the Menu, so it's best to avoid using mobile
+ data for this.
+
+
+ }
+ onClick={handleDecline}
+ size="xs"
+ >
+ Not now
+
+ }
+ onClick={handleAccept}
+ size="xs"
+ >
+ Allow download
+
+
+
+ );
+}
diff --git a/client/components/AiResponse/AiResponseContent.test.tsx b/client/components/AiResponse/AiResponseContent.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..859006dcf7915072786b21ae024f1fe12b7ef6c5
--- /dev/null
+++ b/client/components/AiResponse/AiResponseContent.test.tsx
@@ -0,0 +1,73 @@
+import { MantineProvider } from "@mantine/core";
+import { render, screen } from "@testing-library/react";
+import { usePubSub } from "create-pubsub/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import AiResponseContent from "./AiResponseContent";
+
+vi.mock("create-pubsub/react", () => ({
+ usePubSub: vi.fn(),
+}));
+
+vi.mock("@/modules/textGeneration", () => ({
+ searchAndRespond: vi.fn(),
+}));
+
+vi.mock("@/modules/logEntries", () => ({
+ addLogEntry: vi.fn(),
+}));
+
+vi.mock("./FormattedMarkdown", () => ({
+ default: ({ children }: { children: string }) => (
+