Architecture
You do not need this page to use HelpChatAI. Read it if you want to extend it, host the engine yourself, or understand why a chat panel talks to its own application over HTTP.
Three ideas
1. The engine knows nothing about the UI or the web. Providers, index, scoring, the conversation store and the Markdown conversion are plain Pascal with no Vcl.*, FMX.*, Indy or WebView unit in sight. That is why the same engine serves a VCL application, an FMX one, a console program and a server.
2. Nothing waits on the model. Send records the question, creates a pending assistant message and returns immediately; a worker pool fills it in. A remote model call lasts seconds and never runs on the UI thread. Shutdown waits with a timeout and detaches a stuck worker rather than freezing the application — a detached worker can never touch a freed store.
3. Streaming needs no push channel. Tokens are appended to the pending message in the store; whoever reads the store — the polling page, a UI event — sees the partial grow. No WebSocket, no server-sent events towards the page, nothing to keep alive.
The units
| Unit | Responsibility |
|---|---|
HelpChatAI.Types | Message and context records, cancellation token, retriever contract |
HelpChatAI.Messages | Localized strings (English, Italian), chosen at runtime |
HelpChatAI.Settings | Settings, defaults in one place, JSON load/save |
HelpChatAI.Store | Thread-safe conversation store, pruning, persistence |
HelpChatAI.Runner | Worker pool, cancellation, bounded and memory-safe shutdown |
HelpChatAI.Provider | IHelpChatProvider, registry by name, stub |
HelpChatAI.Provider.DocSearch | Deterministic answers from the index |
HelpChatAI.Provider.Claude | Anthropic Messages API, SSE streaming, grounding, tool use |
HelpChatAI.DocIndex | The index and the keyword retriever (stemming, idf, excerpts) |
HelpChatAI.IndexBuilder | Markdown folder → index JSON |
HelpChatAI.Actions | Registry of actions, schema for the model, the queue to the host thread |
HelpChatAI.Html | Safe Markdown → HTML, URL sanitizing, autolink, bubble fragments |
HelpChatAI.Engine | The facade: send, read, cancel, clear, render, persist |
HelpChatAI.Transport | IHelpChatTransport: what the UI asks for the panel URL |
HelpChatAI.Panel | The panel page and the API contract |
HelpChatAI.HelpViewer | The chat as a System.HelpIntfs viewer (shared by VCL and FMX) |
HelpChatAI.Server | The HTTP layer: sessions, routing, endpoints; the local server |
HelpChatAI.Server.Remote | The multi-session server |
HelpChatAI.Server.HostChannel | The loopback channel a remote page reaches its host through |
HelpChatAI.Transport.Remote | The client of a HelpChatAI server |
Vcl.HelpChatAI*, FMX.HelpChatAI* | The components, the chat control, the VCL style theming |
How the panel talks to the engine
| Endpoint | What it is for |
|---|---|
GET /api/history | The conversation as HTML fragments |
POST /api/send | User bubble + pending assistant bubble |
GET /api/poll/{id} | That bubble, still pending or complete |
POST /api/cancel/{id} | Stop a reply |
POST /api/clear | Start again |
POST /api/context | The screen the user is on (from the native side) |
POST /api/theme | The colours of the host |
POST /api/assistant | The name of the assistant and its icon, for the header |
POST /api/ui/{command} | What the page asks of its host (close) |
Every /api/* call carries X-HelpChatAI-Token: <secret>, generated per process; only the panel page knows it, because it receives it in its URL. The server answers HTML fragments, never JSON: htmx swaps them into the page, which is why the page has almost no JavaScript.
The screen context never travels through the page: the component computes it natively and hands it to the transport. A page cannot forge it and cannot lose it.
Why the page calls its host over HTTP
The page must sometimes ask the application for something — today "close me". WebView2 has a message bridge, but FireMonkey exposes none, and a page served by a remote server has no native side at all. One mechanism works everywhere: an HTTP call to somebody who is listening.
- Local engine: that is the same server, and
/api/ui/closereaches the component. - Remote engine: the client opens a small listener on
127.0.0.1— the host channel — and the server tells the page where it is. CORS is limited to the panel origin, the channel has its own secret, and the browser's private-network preflight is answered.
The same channel is where file exchange between page and application will live.
Threads
| Runs on | What |
|---|---|
| Main thread | The components, the context, the theme, the action handlers |
| Worker pool | Providers: the network call, the SSE parsing, the grounding |
| Indy threads | The HTTP requests of the page |
The store is thread-safe and is the meeting point. OnMessageChanged fires on the thread that changed the message — a UI layer must marshal it; the polling page does not care.
An action crosses the boundary in the other direction: a provider on a worker asks for something that must run on the main thread, and waits with a timeout while honouring cancellation.
Security boundaries
- Model output is untrusted content: rendered through safe-mode Markdown with active HTML escaped, URL schemes restricted to
http(s),mailto,ftpand relative paths, and a Content-Security-Policy on the page. - The browser is confined to the panel origin: any other navigation is cancelled and opened in the system browser.
- The local server binds 127.0.0.1 only and requires the per-process secret.
More in Security and privacy.