- 1 Pitch & positioning
- 2 Use cases
- 3 Layered architecture
- 4 Event flow — from the DOM to the Delphi service
- 5 Technical stack & prerequisites
- 6 Installation & first build
- 7 Recommended start — from the starter project pattern
- 8 Exhaustive instantiation — overview of hooks and options
- 9 API key configuration
- 10 Slash command system
- 11 Loading UI templates
- 12 Writing a custom command plugin
- 13 Configuring capabilities via
ICapabilities - 14 Card system — function, MCP, skill, agent, custom
- 15 Model selector
- 16 Implementing a vendor service (
IVendorServices) - 17 Media output — image, audio, video, TTS
- 18 Audio input (transcription)
- 19 File attachments & file drawer
- 20 Themes & Look & Feel
- 21 Two-step confirmation
- 22 Chat sessions, persistence, pagination
- 23 Plugin UI and JS ↔ Delphi bridge
- 24 Internationalization
- 25 JSON configuration files
- 26 API surface — key interfaces
- 27
TBrowserChatEventevent table - 28 FMX ↔ VCL matrix
- 29 JS template catalog
- 30 Developer mode & diagnostics
- 31 Deployment
- 32 Glossary
- 33 Security & limits
- 34 FAQ / pitfalls
Pythia-Webview2 is a Delphi library that embeds a modern LLM chat interface, similar to ChatGPT or Claude, into a desktop application — both VCL and FMX — by relying on WebView2, which entails an explicit dependency on Windows.
Rendering and interactions happen in HTML/JS; business logic, persistence, security, and extensibility remain in Delphi.
- Delphi vendors who want to integrate a ready-to-use chat UI without rewriting a web frontend.
- Internal-tooling developers who need a brandable chat through JS templates, with business commands shipped as Delphi plugins.
- Multi-vendor integrators — Anthropic, OpenAI, etc. — who want a single frontend wired to their own services.
Pythia-Webview2 is not:
- a vendor SDK;
- a REST wrapper;
- an agent orchestrator.
It is a presentation / control layer that delegates every LLM call to the application code through stable interfaces.
-
AI assistant embedded in a line-of-business application: CRM, ERP, IDE, support tools.
-
Prompt console for developer tools: log analysis, local code review, trace inspection.
-
Multi-model desktop client: model selector, agent cards, skills, MCP.
-
Administration chat interface with slash commands to trigger system actions, for example:
/api-key new anthropic
Pythia-Webview2 is organized around a clear boundary between WebView2 rendering and Delphi services. The HTML/JS side produces events and maintains the visual state; Pascal code receives those messages, validates them, then delegates to injected services. The layers below read top-to-bottom: from the embedded UI down to the application contracts, then to the services and persistence infrastructure.
| Layer | Components |
|---|---|
| UI framework | FMX.WVPythia.Chat |
| WebView2 host + JS templates | uWVFMX*, WebView4Delphi, WebView2 runtimeassets/scripts/*.js — 22 templates |
| Event routing | WVPythia.Chat.EventManager — dispatchWVPythia.Chat.EventHandlers — execution |
| Neutral contracts | WVPythia.AdapterWVPythia.ManagedItemServiceWVPythia.Chat.Interfaces |
| Command layer | Parser → Registry → Plugin (+ApiKey) |
| Services | WVPythia.ApiKey.ServiceWVPythia.ChatSession.ControllerWVPythia.Template.ManagerWVPythia.Capabilities.Manager |
| Infra | WVPythia.JSON.SafeReader/Writer/ResourceWindows.ApiKey.ManagementWVPythia.Net.MediaCodecVCL.WVPythia.OpenDialog / FMX.WVPythia.OpenDialogWindows.Process.Execution |
Important
Note — WebView4Delphi
WebView4Delphi is an open-source project available on GitHub, maintained by salvadordf.
The version included under dependencies/ is not necessarily the latest one.
For project use, it is recommended to clone the official WebView4Delphi repository, then add its source path to the Browser project options under the Source section.
The provided dependencies/ folder is primarily intended to allow quick execution of the examples, mainly to streamline the user experience.
The event layer is independent of the VCL and FMX frameworks: it relies solely on abstract interfaces.
This separation allows roughly 95% of the code to be shared between the two targets.
According to WVPythia.Chat.EventManager.pas:
JS (template)
└─► window.chrome.webview.postMessage({event: "...", ...})
│
▼
TBrowserEventManager.Aggregate(RawJson)
1. CanHandleEvents() ? (readiness invariant)
2. FReader := TJsonReader.Parse(RawJson)
3. EventName = FReader[PROP_EVENT] → TBrowserChatEvent
4. FDispatch[EventKind]() ◄── dispatch table
│
▼
TBrowserEventHandlers (hierarchy of specialized handlers)
Orchestrator / Card / DialogConfirmation /
ChatSession / OpenFile / Selector / Models / Settings
│
▼
Services: IPythiaBrowser, IPersistentChat, IOpenDialog,
IProcessExecute, IChatManagedItemDialogService,
ICommandRegistry, ISecretStore
│
▼
UI feedback: ExecuteScript() / PostWebMessageAsJson()
The event manager is intentionally strict: it processes a message only if the IPythiaBrowser bridge is injected, if the received JSON is valid, and if the event field can be resolved to a TBrowserChatEvent. Once these conditions are met, the method bound to that event in the FDispatch table is called; on an unknown event, the browser receives an error and the message is not executed.
- The JSON is parsed only once into
FReader. - Handlers do not route: they execute.
custom-eventpayloads are never deserialized by the framework: they pass through raw.
Pythia-Webview2 rests on a fixed Windows desktop stack: Delphi hosts the application layer, WebView2 renders the bundled HTML/JS interface, and the application code supplies the LLM/vendor behavior.
| Component | Version / note |
|---|---|
| Delphi | 11 / 12 / 13 — RTTI Generics required. Older versions are not supported. |
| UI frameworks | VCL and/or FMX. |
| Pythia-Webview2 repository | Required locally, with access to source/, assets/, bin32/, bin64/, and the starter projects under demos/VCL/new-projet and demos/FMX/new-projet. |
| WebView2 Runtime | Microsoft Edge WebView2 Evergreen Runtime installed or deployable on the target machine. |
| WebView4Delphi | The repository contains a bundled copy under dependencies/WebView4Delphi/source, useful for demos and quick discovery. For real application work, clone the official WebView4Delphi repository and add that source path to the Delphi project. |
| Platform | Windows x86 / x64 — binaries conventionally produced in bin32 / bin64. |
| Secret storage | Default implementation through Windows.ApiKey.Management.pas; applications may replace the backend via ISecretStore. |
| JSON | System.JSON plus the framework helpers such as WVPythia.JSON.SafeReader.pas. |
You do not need npm, Node.js, Python, Docker, or a web build chain to use the shipped interface. The JavaScript layer is already bundled in assets/.
The recommended way to start is not to create a Delphi project from scratch. The recommended path is to clone one of the minimal starter project patterns provided with the repository:
<Pythia-Webview2>\demos\VCL\new-projet
or:
<Pythia-Webview2>\demos\FMX\new-projet
Use the first one for a VCL application and the second one for an FMX application. These starter projects already contain the expected structure, the browser setup, and the ready-to-use service unit:
VCL.WVPythia.Services.pas
or:
FMX.WVPythia.Services.pas
Starting from these templates avoids the most common setup errors: missing ServiceAdapter, incorrect output folder, missing assets, or missing WebView2Loader.dll.
A fully manual setup from an empty Delphi project is still possible, but it should be treated as an advanced path used mainly to understand the wiring.
Copy the matching folder, then rename the folder, the Delphi project, and the files as needed for your application.
<Pythia-Webview2>\demos\VCL\new-projet
<Pythia-Webview2>\demos\FMX\new-projet
In Delphi, open:
Tools → Options → IDE → Environment Variables
Create a variable pointing to the Pythia-Webview2 repository:
PYTHIA = C:\Path\To\Pythia-Webview2
It is also recommended to clone the official WebView4Delphi repository and create a second variable:
WEBVIEW4DELPHI = C:\Path\To\WebView4Delphi
In:
Project → Options → Building → Delphi Compiler → Search path
add at least:
$(PYTHIA)\source
$(WEBVIEW4DELPHI)\source
The bundled WebView4Delphi path can be used for demos and quick discovery:
$(PYTHIA)\dependencies\WebView4Delphi\source
For a real application, prefer the official WebView4Delphi clone.
Pythia-Webview2 resolves several paths from the executable location. Keep a consistent structure around bin32, bin64, and assets.
Use:
..\bin32
for Win32, or:
..\bin64
for Win64.
Set the DCU output directory to:
..\dcu
During early development, compiling directly into the Pythia-Webview2 repository folders is also possible:
$(PYTHIA)\bin32
$(PYTHIA)\bin64
This is convenient because WebView2Loader.dll and assets are already in the expected layout.
WebView2Loader.dll must be placed next to the executable and must match the target architecture.
For Win32:
$(PYTHIA)\bin32\WebView2Loader.dll
For Win64:
$(PYTHIA)\bin64\WebView2Loader.dll
Without this file, the application may start, but WebView2 will not initialize correctly.
The assets folder must be available at the same level as bin32 and bin64.
Recommended structure:
MyProject
├── assets
│ ├── index.htm
│ ├── scripts
│ ├── media
│ └── lang
├── bin32
│ ├── MyProject.exe
│ └── WebView2Loader.dll
├── bin64
│ ├── MyProject.exe
│ └── WebView2Loader.dll
└── dcu
For deployment, ship your own complete copy of assets with your executable.
The first goal is not merely to display a WebView. The first goal is to run a minimal Pythia-Webview2 project whose browser surface and Delphi event chain are already wired.
Open the project copied from:
<Pythia-Webview2>\demos\VCL\new-projet
or:
<Pythia-Webview2>\demos\FMX\new-projet
The starter project already contains:
- the Delphi host application;
- the WebView2-backed chat surface;
- the browser setup;
- the service adapter unit;
- the expected output structure;
- the support folder generation path.
The essential VCL ordering is:
uses
VCL.WVPythia.Chat,
WVPythia.Types,
VCL.WVPythia.Services;
procedure TForm1.FormCreate(Sender: TObject);
begin
Pythia := TVCLPythia.Create(Panel2);
Pythia.ServiceAdapter := TVCLChatManagedItemDialogService.Create;
Pythia.Update;
end;The essential FMX ordering is:
uses
FMX.WVPythia.Chat,
WVPythia.Types,
FMX.WVPythia.Services;
procedure TForm1.FormCreate(Sender: TObject);
begin
Pythia := TFMXPythia.Create(Layout1);
Pythia.AttachHost(Self);
Pythia.ServiceAdapter := TFMXChatManagedItemDialogService.Create;
Pythia.Update;
end;The order is critical:
Create browser
Attach host if FMX
Assign ServiceAdapter
Call Update
If the adapter is missing, or if it is assigned after Update, the UI can render but prompt submissions and other browser events will not reach Delphi code.
Press F9.
You should see:
- the form opens;
- the WebView2 host warms up;
- the Pythia-Webview2 chat interface appears;
- the input bar is visible at the bottom;
- prompt submission reaches the service adapter.
Depending on the starter behavior, the adapter may display a diagnostic response, a JSON dump, or a placeholder result. Replacing that diagnostic behavior with a real vendor call is covered in §16.
On first launch, Pythia-Webview2 creates an application support folder such as:
bin32\MyProject\support
or:
bin64\MyProject\support
This folder contains generated or pre-filled JSON configuration files: capabilities, model list, runtime category configuration, cards, and other support files.
Important
After creating the TVCLPythia or TFMXPythia instance and wiring the required initialization code, it is recommended to run the application once as early as possible.
This first execution lets Pythia-Webview2 create the application support folder and generate the initial JSON configuration files. From that point on, you can inspect, pre-fill, or replace those files with your application-specific configuration instead of working against a folder structure that does not exist yet.
The recommended starter projects give you the minimal correct wiring. A production application then adds optional panels, lifecycle hooks, capability configuration, and vendor-specific initialization.
Three families of customization points coexist:
- state properties such as
CustomPanelsandEnabledButtons, which enable or delegate parts of the UI; - the service adapter through
ServiceAdapter, which is the boundary between WebView2 events and application logic; - lifecycle hooks such as
OnBrowserCreated,OnThemeChanged,OnTranslationsLoaded,OnRenderChatContent,OnApiKeyChanged,OnChatSessionAutoRename, andOnRegisterCommandPlugins.
The call to Update must remain last. It starts the WebView2 bootstrap and may immediately consume the values configured on the component.
uses
VCL.WVPythia.Chat, WVPythia.Types, WVPythia.Adapter;
procedure TForm1.FormCreate(Sender: TObject);
begin
{--- Component creation. The parent (Panel2) must exist at this point
and will host the WebView2 host. }
Pythia := TVCLPythia.Create(Panel2);
{--- Disables default rendering for the following panels: the host
application provides its own implementation.
cpCards : card management panel (MCP, function, skill, agent)
cpModels : model selector
cpSettings : settings panel (temperature, top-p, system prompt, etc.)
Union with the current value lets you cumulate with the defaults. }
Pythia.CustomPanels :=
Pythia.CustomPanels + [cpCards, cpModels, cpSettings];
{--- Enables optional buttons in the input bar:
ebSettings : shortcut to open the settings panel
ebMicrophone : audio input (not enabled here) }
Pythia.EnabledButtons :=
Pythia.EnabledButtons + [ebSettings];
{--- Application adapter implementing IChatManagedItemDialogService
(WVPythia.Adapter.pas). Targets: item selection (cards),
message or code copy, session creation, and most importantly
reception of JS custom-events via ActivateCustomEvent.
TChatManagedItemDialogService is a class that the host
application must provide. }
Pythia.ServiceAdapter := TMyChatManagedItemDialogService.Create;
Pythia.OnBrowserCreated :=
procedure
begin
{--- Called once, after WebView2 initialization and the initial
navigation (VCL.WVPythia.Chat.pas).
Entry point for post-init configuration that requires the
WebView instance to be ready: cookie placement, telemetry,
global script injection, first ExecuteScript. }
end;
Pythia.OnThemeChanged :=
procedure
begin
{--- Triggered when a look-and-feel change is applied through
the settings panel (VCL.WVPythia.Chat.pas).
Synchronize here the host application's chrome — window
colors, toolbars, icons — with the theme selected in the
chat. }
end;
Pythia.OnTranslationsLoaded :=
procedure
begin
{--- Called after every dictionary reload, right after the
native S_… strings of WVPythia.Strs have been retranslated
(WVPythia.Strs.pas). This is where the host application's
custom-string retranslation belongs (see §24 — custom
section of the language JSON). }
end;
Pythia.OnRenderChatContent :=
function: Boolean
begin
{--- Lets the host intercept chat-content rendering before the
standard pipeline runs (VCL.WVPythia.Chat.pas).
Return True to short-circuit the default rendering, False
to let Pythia-Webview2 continue. Use case: inject a dynamic
welcome screen or restore an archived session with
specific rendering. }
Result := False;
end;
Pythia.OnApiKeyChanged :=
procedure (Value: string)
begin
{--- Called after creation, modification, or deletion of an
API key via /api-key new|delete (VCL.WVPythia.Chat.pas).
Value = normalized name (Trim + lowercase). Use this hook
to invalidate cached vendor clients, rebuild services
that consume the key, update the host UI that reflects
the connection state. }
end;
Pythia.OnChatSessionAutoRename :=
procedure (Value1, Value2: string)
begin
{--- Triggered when the session controller requests an
automatic title for a new conversation
(VCL.WVPythia.Chat.pas).
Value1 = session ID
Value2 = textual content to use as a naming reference
Call the LLM to generate a short title, then persist it
via Pythia.SessionAutoRename(Value1, NewTitle). }
end;
Pythia.OnRegisterCommandPlugins :=
procedure
begin
{--- Called during CommandLineInitialize, after the automatic
registration of TApiKeyPlugin (VCL.WVPythia.Chat.pas).
Plug in the application's business command plugins here
via Pythia.CommandLine.RegisterPlugin(...) — see §12
for the custom plugin skeleton. }
end;
{--- Optional vendor-provided service called when a file is selected through
the open dialog. Set it from the host bootstrap to enable remote file
transfer (Files API and similar). When unset, files keep flowing through
the existing inline pipeline unchanged. }
Pythia.FileUploadService := "instance of" IFileUploadService;
{--- Optional vendor-provided service invoked when a TOpenFileTarget.Knowledge
file is selected. Set it from the host bootstrap to enable remote
vectorization (vector store, semantic retrieval corpus, libraries, …)
and reference indexed files at submit time through TryGetIndexRef.
When unset, knowledge files remain in the compose box but are not
processed asynchronously. }
Pythia.KnowledgeIndexingService := "instance of" IKnowledgeIndexingService;
Pythia.OnInitialized :=
procedure
begin
{--- Called after the complete chat runtime initialization
(VCL.WVPythia.Chat.pas).
At this point WebView2 is created, the page has emitted "ready",
the JS templates have been injected, capabilities/settings/model
list/session drawer have been synchronized, enabled buttons have
been applied, and the input surface has been focused.
This is the safe post-init hook for actions that require the
Pythia-Webview2 chat UI to be operational: first prompt/display,
late host synchronization, service refresh, or custom JS relying
on the injected runtime.
Difference with OnBrowserCreated:
OnBrowserCreated only means that the native WebView2 instance
exists and initial navigation has started. OnInitialized means
that the chat bridge and UI runtime are ready. }
end;
{--- Starts the WebView2 bootstrap. Must be called last, after every
property and hook has been assigned. }
Pythia.Update;
end;uses
FMX.WVPythia.Chat, WVPythia.Types, WVPythia.Adapter;
procedure TForm1.FormCreate(Sender: TObject);
begin
{--- Component creation: the parent (Layout1) will host the FMX-
specific WebView2 host. }
Pythia := TFMXPythia.Create(Layout1);
{--- Mandatory link between the component and the FMX form.
FMX-specific: without this call, the WebView host cannot
attach itself to the FireMonkey rendering cycle. }
Pythia.AttachHost(Self);
{--- Disables default rendering for the following panels: the host
application provides its own implementation.
cpCards : card management panel (MCP, function, skill, agent)
cpModels : model selector
cpSettings : settings panel (temperature, top-p, system prompt, etc.) }
Pythia.CustomPanels :=
Pythia.CustomPanels + [cpCards, cpModels, cpSettings];
{--- Enables optional buttons in the input bar:
ebSettings : shortcut to open the settings panel
ebMicrophone : audio input (not enabled here) }
Pythia.EnabledButtons :=
Pythia.EnabledButtons + [ebSettings];
{--- Application adapter implementing IChatManagedItemDialogService
(WVPythia.Adapter.pas). Targets: item selection (cards),
message or code copy, session creation, and most importantly
reception of JS custom-events via ActivateCustomEvent.
TChatManagedItemDialogService is a class that the host
application must provide. }
Pythia.ServiceAdapter := TMyChatManagedItemDialogService.Create;
Pythia.OnBrowserCreated :=
procedure
begin
{--- Called once, after WebView2 initialization and the initial
navigation. Entry point for post-init configuration that
requires the WebView instance to be ready: cookie placement,
telemetry, global script injection, first ExecuteScript. }
end;
Pythia.OnThemeChanged :=
procedure
begin
{--- Triggered when a look-and-feel change is applied through
the settings panel. Synchronize here the host application's
chrome — window colors, toolbars, icons — with the theme
selected in the chat. }
end;
Pythia.OnTranslationsLoaded :=
procedure
begin
{--- Called after every dictionary reload, right after the
native S_… strings of WVPythia.Strs have been retranslated.
This is where the host application's custom-string
retranslation belongs (see §24 — custom section of the
language JSON). }
end;
Pythia.OnRenderChatContent :=
function: Boolean
begin
{--- Lets the host intercept chat-content rendering before the
standard pipeline runs. Return True to short-circuit the
default rendering, False to let Pythia-Webview2 continue. Use case:
inject a dynamic welcome screen or restore an archived
session with specific rendering. }
Result := False;
end;
Pythia.OnApiKeyChanged :=
procedure (Value: string)
begin
{--- Called after creation, modification, or deletion of an API key
via /api-key new|delete. Value = normalized name (Trim + lowercase).
Use this hook to invalidate cached vendor clients, rebuild
services that consume the key, update the host UI that
reflects the connection state. }
end;
Pythia.OnChatSessionAutoRename :=
procedure (Value1, Value2: string)
begin
{--- Triggered when the session controller requests an automatic
title for a new conversation.
Value1 = session ID
Value2 = textual content to use as a naming reference
Call the LLM to generate a short title, then persist it
via Pythia.SessionAutoRename(Value1, NewTitle). }
end;
Pythia.OnRegisterCommandPlugins :=
procedure
begin
{--- Called during CommandLineInitialize, after the automatic
registration of TApiKeyPlugin. Plug in the application's
business command plugins here via
Pythia.CommandLine.RegisterPlugin(...) — see §12 for
the custom plugin skeleton. }
end;
{--- Optional vendor-provided service called when a file is selected through
the open dialog. Set it from the host bootstrap to enable remote file
transfer (Files API and similar). When unset, files keep flowing through
the existing inline pipeline unchanged. }
Pythia.FileUploadService := "instance of" IFileUploadService;
{--- Optional vendor-provided service invoked when a TOpenFileTarget.Knowledge
file is selected. Set it from the host bootstrap to enable remote
vectorization (vector store, semantic retrieval corpus, libraries, …)
and reference indexed files at submit time through TryGetIndexRef.
When unset, knowledge files remain in the compose box but are not
processed asynchronously. }
Pythia.KnowledgeIndexingService := "instance of" IKnowledgeIndexingService;
Pythia.OnInitialized :=
procedure
begin
{--- Called after the complete chat runtime initialization
(VCL.WVPythia.Chat.pas).
At this point WebView2 is created, the page has emitted "ready",
the JS templates have been injected, capabilities/settings/model
list/session drawer have been synchronized, enabled buttons have
been applied, and the input surface has been focused.
This is the safe post-init hook for actions that require the
Pythia-Webview2 chat UI to be operational: first prompt/display,
late host synchronization, service refresh, or custom JS relying
on the injected runtime.
Difference with OnBrowserCreated:
OnBrowserCreated only means that the native WebView2 instance
exists and initial navigation has started. OnInitialized means
that the chat bridge and UI runtime are ready. }
end;
{--- Starts the WebView2 bootstrap. Must be called last, after every
property and hook has been assigned. }
Pythia.Update;
end;| Component | Family | Type | Role |
|---|---|---|---|
CustomPanels |
UI state | TCustomPanels set |
Delegates Settings / Models / Cards panels to the host application. |
EnabledButtons |
UI state | TEnabledButtons set |
Enables optional buttons such as Settings or Microphone. |
ServiceAdapter |
Application boundary | IChatManagedItemDialogService |
Routes prompts, selections, copies, custom events, and application actions toward Delphi logic. |
OnBrowserCreated |
Lifecycle | TProc |
Post-initialization WebView2 hook. |
OnThemeChanged |
Lifecycle | TProc |
Reacts to look-and-feel changes. |
OnTranslationsLoaded |
Lifecycle | TProc |
Reloads host custom translations after dictionary loading. |
OnRenderChatContent |
Lifecycle | TFunc<Boolean> |
Lets the host intercept chat-content rendering. |
OnApiKeyChanged |
Lifecycle | TProc<string> |
Notifies key creation, modification, or deletion. |
OnChatSessionAutoRename |
Lifecycle | TProc<string,string> |
Requests a generated title for a session. |
OnRegisterCommandPlugins |
Lifecycle | TProc |
Registers business slash-command plugins. |
ApiKeySecretStore |
Application boundary | ISecretStore |
Reads, writes, and deletes API key values. |
OnInitialized |
Lifecycle | TProc |
Runs after the complete chat runtime initialization; safe hook for actions requiring the WebView2 chat UI, bridge, templates, capabilities/settings/models, buttons, and input surface to be ready. |
The assignment:
Pythia.ServiceAdapter := TVCLChatManagedItemDialogService.Create;or:
Pythia.ServiceAdapter := TFMXChatManagedItemDialogService.Create;is not cosmetic. It is the routing layer that lets the WebView2 event pipeline reach the application code.
The official new-projet starter patterns already include the matching service unit:
VCL.WVPythia.Services.pas
FMX.WVPythia.Services.pas
You normally adapt that unit. You do not need to copy the adapter from pythia-sample when starting from new-projet.
The component knows the outside world through IChatManagedItemDialogService. Prompt submission, message copy, code-block copy, card selection, settings requests, audio input, new-chat events, and custom-event messages are all routed through this interface.
If the adapter is missing, or assigned after Update, the interface may render but the downstream event chain is unavailable. A typical diagnostic is:
DialogService not assigned
Typical causes are:
ServiceAdapterwas not assigned;ServiceAdapterwas assigned afterPythia.Update;- the service unit exists on disk but was not added to the Delphi project;
- the VCL service unit was used in an FMX project, or the reverse;
- abstract methods were not overridden when a custom adapter was written manually.
The service unit usually contains two complementary types.
1. An adapter class — by convention TVCLChatManagedItemDialogService or TFMXChatManagedItemDialogService — inheriting from TCustomChatManagedItemDialogService. The parent class implements the common interface; the project-specific class overrides the Do... methods needed by the application.
2. A static-method container — by convention TToolContainer — that hosts the routing/business entry points. The adapter remains thin and delegates to this container.
Once assigned before Update, the chain becomes:
WebView2 DOM
│
└── postMessage
│
▼
TBrowserEventManager
│
▼
ServiceAdapter
│
▼
TToolContainer / application service methods
│
▼
Business logic / vendor service
The adapter should remain a routing layer, not the place where SDK-specific logic accumulates.
The sample projects remain useful references:
demos\VCL\pythia-sample\VCL.WVPythia.Services.pasis a diagnostic skeleton for understanding event routing;demos\VCL\pythia-anthropic\VCL.WVPythia.Services.passhows a real vendor wiring throughAnthropicVendor: IVendorServices.
Use them as references, or copy one manually only if you deliberately started from an empty Delphi project or want to replace the starter service unit with a more specific implementation.
The recommended approach is to start from the official starter pattern, not from a copied sample adapter and not from an empty Delphi project. The pythia-sample and pythia-anthropic projects remain useful references, but they are not the normal starting point for a new application.
The normal sequence is the following.
1. Clone the project pattern
Copy the folder that matches your framework:
<Pythia-Webview2>\demos\VCL\new-projet
or:
<Pythia-Webview2>\demos\FMX\new-projet
Use the first one for a VCL application and the second one for an FMX application. Then rename the folder, the Delphi project, and the files as needed for your application.
The new-projet templates are intended to be minimal starting points for applications based on Pythia-Webview2. They already contain the expected structure, the browser setup, and the ready-to-use service unit:
VCL.WVPythia.Services.pas
or:
FMX.WVPythia.Services.pas
Starting from these templates avoids the common setup issues: missing ServiceAdapter, incorrect output folder, missing assets, or missing WebView2Loader.dll.
2. Define IDE environment variables in Delphi
In Delphi, open:
Tools → Options → IDE → Environment Variables
Create a variable pointing to the Pythia-Webview2 repository:
PYTHIA = C:\Path\To\Pythia-Webview2
It is also recommended to clone the official WebView4Delphi repository and create a second variable:
WEBVIEW4DELPHI = C:\Path\To\WebView4Delphi
These variables let the project use portable paths such as:
$(PYTHIA)\source
$(WEBVIEW4DELPHI)\source
This makes the project easier to move between machines or folders without rewriting all paths manually.
3. Configure the project search paths
In the Delphi project options, open:
Project → Options → Building → Delphi Compiler → Search path
Add at least:
$(PYTHIA)\source
$(WEBVIEW4DELPHI)\source
Pythia-Webview2 also includes a copy of WebView4Delphi under:
$(PYTHIA)\dependencies\WebView4Delphi\source
This bundled path is useful for compiling demos quickly and for first discovery. For a real project, prefer the official WebView4Delphi clone:
$(WEBVIEW4DELPHI)\source
If WebView4Delphi code is redistributed with your project or kept in your repository, preserve the original license and copyright notices in the corresponding third-party files.
4. Configure the output directory
Pythia-Webview2 resolves several paths from the executable location. Keep a consistent folder structure around bin32, bin64, and assets.
In:
Project → Options → Building → Delphi Compiler → Output directory
use:
..\bin32
for a Win32 target, or:
..\bin64
for a Win64 target.
Set the DCU output directory to:
..\dcu
During early development, you may also compile directly into the Pythia-Webview2 repository folders:
$(PYTHIA)\bin32
or:
$(PYTHIA)\bin64
This can be useful because WebView2Loader.dll and the assets folder are already available in the expected structure. For a standalone application, keep your own project structure and copy the required files into it.
5. Check that WebView2Loader.dll is present
WebView2Loader.dll must be placed next to the executable and must match the target architecture.
For a 32-bit application, copy:
$(PYTHIA)\bin32\WebView2Loader.dll
to:
<MyProject>\bin32\WebView2Loader.dll
For a 64-bit application, copy:
$(PYTHIA)\bin64\WebView2Loader.dll
to:
<MyProject>\bin64\WebView2Loader.dll
Without this file, the application may start, but WebView2 will not initialize correctly.
6. Check the assets folder
The assets folder must be available at the same level as bin32 and bin64.
Recommended structure:
MyProject
├── assets
│ ├── index.htm
│ ├── scripts
│ ├── media (empty)
│ └── lang
├── bin32
│ ├── MyProject.exe
│ └── WebView2Loader.dll
├── bin64
│ ├── MyProject.exe
│ └── WebView2Loader.dll
└── dcu
The assets folder contains the HTML interface, JavaScript scripts, media folder, and language files used by Pythia-Webview2. Copy it as a complete folder.
If you compile directly into:
$(PYTHIA)\bin32
or:
$(PYTHIA)\bin64
you can rely on:
$(PYTHIA)\assets
during development, without copying the assets into your own project immediately. For deployment, ship your own copy of the assets folder with your executable.
7. Install or verify the Microsoft WebView2 Evergreen Runtime
There are two separate elements to keep in mind:
WebView2Loader.dll
and the Microsoft Edge WebView2 Runtime.
WebView2Loader.dll is the DLL loaded by your application. The WebView2 Runtime is the Microsoft Edge-based runtime used to render the web interface.
The target machine must have the Microsoft Edge WebView2 Evergreen Runtime installed. On recent Windows versions, it is often already present with Microsoft Edge. If it is not present, install it separately.
The Evergreen Runtime is the recommended deployment mode for most applications because it is installed on the machine and updated automatically by Microsoft. The official Microsoft download page provides the Evergreen Bootstrapper, the Evergreen Standalone Installer, and x86, x64, and ARM64 versions.
8. Recommended final structure
For a standalone project, the expected structure is:
MyProject
├── assets
│ ├── index.htm
│ ├── scripts
│ ├── media (empty)
│ └── lang
├── bin32
│ ├── MyProject.exe
│ └── WebView2Loader.dll
├── bin64
│ ├── MyProject.exe
│ └── WebView2Loader.dll
├── dcu
├── Main.pas
├── Main.dfm or Main.fmx
├── VCL.WVPythia.Services.pas
│ or
├── FMX.WVPythia.Services.pas
└── MyProject.dproj
On first launch, Pythia-Webview2 creates the application support folder, for example:
bin32\MyProject\support
or:
bin64\MyProject\support
This folder contains the generated or pre-filled JSON configuration files: capabilities, model list, category configuration, cards, and other support files. These files can be pre-populated before deployment so the application starts with a controlled configuration.
Summary
To start as simply as possible:
- Copy
demos\VCL\new-projetordemos\FMX\new-projet. - Rename the project for your application.
- Create a
PYTHIAIDE environment variable pointing to the Pythia-Webview2 repository. - Clone the official WebView4Delphi repository.
- Create a
WEBVIEW4DELPHIIDE environment variable pointing to that repository. - Configure the Search path with
$(PYTHIA)\sourceand$(WEBVIEW4DELPHI)\source. - Configure the Output directory as
..\bin32or..\bin64. - Make sure
WebView2Loader.dllis next to the executable. - Make sure the
assetsfolder is available at the correct level. - Make sure the Microsoft Edge WebView2 Evergreen Runtime is installed.
- Verify that
ServiceAdapteris assigned beforeUpdate. - Run the project and then replace the diagnostic behavior in
TToolContainer.ActivateInputStatewith a real vendor call.
By starting from the new-projet templates, you get a clean baseline for integrating Pythia-Webview2 while keeping full control over paths, dependencies, and deployment structure.
API key management is built natively into Pythia-Webview2 as a preinstalled command plugin. The end user can manage keys directly from the chat input bar, and the application can also trigger the same command flow programmatically when a vendor service starts and detects that its key is missing.
The implementation rests on four units:
WVPythia.Command.Plugin.ApiKey.pas— command plugin (TApiKeyPlugin);WVPythia.ApiKey.Service.Intf.pas—IApiKeyServicecontract andTApiKeyOperationResult;WVPythia.ApiKey.Service.pas— default service (TApiKeyService);Windows.ApiKey.Management.pas— default secret store (TSecretStore).
- User surface in the chat
- Actual signatures
- Rules
- Custom security policy
- Vendor-owned key name
- Automatic first-run API-key request
- Delphi-side usage
/api-key new <name> → opens a hidden input, stores the secret
/api-key delete <name> → removes the secret + JSON entry
/api-key exists <name> → reports presence
For example:
/api-key new anthropic
Declared in WVPythia.ApiKey.Service.Intf.pas:
IApiKeyService = interface
['{A7C3E812-4D5F-4B91-8E2A-9F6B3C7D1E40}']
function GetBrowser: IPythiaBrowser;
procedure SetBrowser(const Value: IPythiaBrowser);
function CreateKey(const AName: string): TApiKeyOperationResult;
function DeleteKey(const AName: string): TApiKeyOperationResult;
function Exists(const AName: string): Boolean;
property Browser: IPythiaBrowser read GetBrowser write SetBrowser;
end;TApiKeyOperationResult is a record with two fields, Success: Boolean and Message: string, plus two class constructors Ok and Fail. There is no IsOk property.
An API key is identified by a logical name, not by its value.
- Names are normalized:
Trim.ToLowerInvariant. Anthropicis therefore equivalent toanthropic.- The name is persisted in a tracking JSON file.
- The secret value is read and written through
IPythiaBrowser.ApiKeySecretStore(ISecretStore). - After any mutation,
IPythiaBrowser.ApiKeyValuesUpdatenotifies UI consumers.
TApiKeyService never needs to access the Windows Registry directly. Reads and writes go through FBrowser.ApiKeySecretStore, which holds an ISecretStore implementation.
ISecretStore = interface
['{0828CA5A-491F-41E5-B127-9037F22CCF79}']
function ReadSecret(const Name: string; out Value: string;
const ParamProc: TProc<string> = nil): Boolean;
procedure WriteSecret(const Name, Value: string);
procedure DeleteSecret(const Name: string);
end;ApiKeySecretStore is a public property on TFMXPythia / TVCLPythia, initialized to the default store in the constructor. To substitute another backend, implement ISecretStore and assign it before Update:
uses WVPythia.Chat.Interfaces;
Pythia.ApiKeySecretStore := TMySecretStore.Create;
Pythia.Update;A vendor service should own the logical key name it uses. Do not duplicate this string across the application.
For Anthropic, define the key name on the service class:
TAnthropicServices = class(TInterfacedObject, IVendorServices)
const
API_KEY_NAME = 'anthropic';
private
FClient: IAnthropic;
FBrowser: IPythiaBrowser;
public
constructor Create(const ABrowser: IPythiaBrowser);
procedure UpdateApiKey;
end;The value anthropic is the name used by the browser secret store. Keep it consistent everywhere the Anthropic service reads, creates, or refreshes the key.
The user should not be forced to type /api-key new anthropic manually on first launch. A vendor service can trigger the same command automatically when it starts and the key does not already exist:
constructor TAnthropicServices.Create(const ABrowser: IPythiaBrowser);
var
Anthropic_key: string;
begin
FBrowser := ABrowser;
if not FBrowser.ApiKeySecretStore.ReadSecret(API_KEY_NAME, Anthropic_key) then
FBrowser.TryHandleAsCommand(Format('/api-key new %s', [API_KEY_NAME]));
FClient := TAnthropicFactory.CreateInstance(Anthropic_key);
end;When the key is added or changed, reload the SDK client from the same constant:
procedure TAnthropicServices.UpdateApiKey;
var
Anthropic_key: string;
begin
if not FBrowser.ApiKeySecretStore.ReadSecret(API_KEY_NAME, Anthropic_key) then
begin
FClient.API.Token := '';
Exit;
end;
FClient.API.Token := Anthropic_key;
FBrowser.DisplaySuccess('Anthropic client is up to date.');
end;A typical host-side wiring is:
procedure TForm1.FormCreate(Sender: TObject);
begin
Pythia := TVCLPythia.Create(Panel2);
Pythia.ServiceAdapter := TVCLChatManagedItemDialogService.Create;
Pythia.OnApiKeyChanged := UpdateApiKey;
Pythia.Update;
end;
procedure TForm1.UpdateApiKey(KeyName: string);
begin
if SameText(KeyName, TAnthropicServices.API_KEY_NAME) then
AnthropicVendor.UpdateApiKey;
end;TApiKeyPlugin is registered automatically by the component during command-line initialization. The slash-command surface requires no modification, but vendor services should still own their key name and refresh their SDK client through OnApiKeyChanged.
The command layer defines a native extension point for the browser: it allows you to associate textual commands of the form /command action with explicitly declared Delphi handlers, validated by the registry, then executed by dedicated plugins.
/command-name action [arg1] [arg2] ...
parse (TCommandParser)
→ validate (TCommandRegistry)
→ execute (TCommandPlugin.DoExecute)
csNotACommandcsOkcsUnknownCommandcsUnknownActioncsWrongArgCount
Execute wraps DoExecute in a try/except block.
- Expected failure:
TCommandExecResult.Fail(...). - Unexpected exception: automatically converted into a failure result.
When user input is not a command, that is csNotACommand, the framework falls back on the standard prompt flow. There is nothing to handle on the developer side.
The chat interface is not a monolithic rendering: it is composed from one root HTML file and 22 JavaScript files injected into WebView2. Loading is driven by ITemplateProvider (WVPythia.Template.Manager.pas).
- Locations
- TTemplateType enumeration (23 entries)
- Two loading strategies
- Replacing a template without recompiling
- Conventions observed in the shipped templates
assets/index.htm ← HTML skeleton (main_html)
assets/scripts/*.js ← 22 JS templates (PromptTemplate, DisplayTemplate, ...)
Defined in WVPythia.Template.Manager.pas. Each value is mapped to a file via FileNames (WVPythia.Template.Manager.pas).
ITemplateProvider exposes two methods (WVPythia.Template.Manager.pas):
| Method | Effect |
|---|---|
TemplateAllwaysReloading(APath) |
Reads the file from disk on every access. Dev mode: JS edits are picked up without recompilation. Note: the original name preserves the typo "Allways". |
TemplateNeverReloading |
Loads once, keeps in memory. Production mode. |
The dev notes (
WVPythia.Template.Manager.pas) state: "no advanced caching logic — goal = clarity & simplicity for the community".
Provider.LoadCustomTemplate('scripts\MyPromptTemplate.js');LoadCustomTemplate (WVPythia.Template.Manager.pas) lets you substitute a template with an alternative file. Combined with TemplateAllwaysReloading, this is the lightest UI extension point — edit a JS file, restart the app, see the result.
The shipped templates are simple JavaScript fragments loaded into the same WebView context. They must therefore stay self-contained, limit global exports, and only publish on window the APIs that are deliberately shared. The framework does not validate their internal structure; it only observes the messages sent to the WebView2 bridge.
The following patterns are observed in the 22 shipped templates, not enforced by the framework:
- IIFE
(() => { ... })();to encapsulate local state. - Internationalization via
window.AppI18n.t('key')— exposed byBootstrapDictionaryTemplate.js. - Event emission via
window.chrome.webview.postMessage({event, ...}).
The only contract truly enforced by Delphi is: all communication toward the Pascal code goes through window.chrome.webview.postMessage. The rest is the template author's freedom.
Modeled on TApiKeyPlugin (WVPythia.Command.Plugin.ApiKey.pas):
type
TMyPlugin = class(TCommandPlugin)
strict private
FService: IMyService;
strict protected
function DoExecute(const Action: string;
const Args: TArray<string>): TCommandExecResult; override;
public
constructor Create(const AService: IMyService);
end;
constructor TMyPlugin.Create(const AService: IMyService);
begin
inherited Create('my-command'); // slash name → /my-command
FService := AService;
AddAction('run', 1, 1); // AddAction(name, minArgs, maxArgs)
AddAction('status', 0, 0);
end;
function TMyPlugin.DoExecute(const Action: string;
const Args: TArray<string>): TCommandExecResult;
begin
if Action = 'run' then
Result := TCommandExecResult.Ok(FService.Run(Args[0]))
else if Action = 'status' then
Result := TCommandExecResult.Ok('running')
else
Result := TCommandExecResult.Fail('Unmanaged action');
end;The skeleton above corresponds to the form expected by the registry. Command validation rests on the actions declared in the constructor; DoExecute should therefore only contain the execution logic for actions already announced by AddAction.
TCommandPlugininherits fromTCommandSpec—Create(AName)andAddAction(AName, AMinArgs, AMaxArgs)come from the parent class (WVPythia.Command.Parser.pas).DoExecuteisabstract, with the fixed signature(Action: string; Args: TArray<string>): TCommandExecResult(WVPythia.Command.Plugin.pas).TCommandExecResult.Ok(msg)/.Fail(msg)are class functions (WVPythia.Chat.Interfaces.pas).
Custom plugins are wired through the published property OnRegisterCommandPlugins: TProc of the component (FMX.WVPythia.Chat.pas, VCL.WVPythia.Chat.pas). The registry itself is reachable via CommandLine: ICommandRegistry (WVPythia.Chat.Interfaces.pas).
procedure TForm1.FormCreate(Sender: TObject);
begin
Pythia := TFMXPythia.Create(Layout1);
Pythia.AttachHost(Self);
Pythia.OnRegisterCommandPlugins :=
procedure
begin
Pythia.CommandLine.RegisterPlugin(TMyPlugin.Create(FMyService));
end;
Pythia.Update;
end;The hook is invoked by the component after CommandLineInitialize (FMX.WVPythia.Chat.pas), therefore after the automatic registration of TApiKeyPlugin. Custom plugins are added; they do not replace existing ones.
When Pythia-Webview2 starts, the application decides which features the UI exposes: thinking controls, vision, file attachment, web search, integration cards, media creation, model selector, and related surfaces. These choices are carried by ICapabilities (WVPythia.Capabilities.Manager.pas) and persisted in the application support folder.
On first launch, the generated file is stored under a path such as:
bin32\MyProject\support\MyProject-capabilities.json
or:
bin64\MyProject\support\MyProject-capabilities.json
Capabilities are declared in the TFunctionsType enum and grouped into coherent families.
| Capability group | Purpose |
|---|---|
Endpoint* |
Surfaces tied to the LLM endpoint families. |
Thinking and thinking tiers |
Reasoning-related controls. |
Vision |
Image input and vision-related UI. |
Files and KnowledgeSearch |
File and knowledge-related input surfaces. |
Integration* |
Function, MCP, skill, agent, and custom card families. |
Media* |
Image, audio, video, speech-to-text, and text-to-speech surfaces. |
WebSearch and DeepResearch |
Research-oriented tools. |
Model |
Model selector visibility. |
Custom and SystemPrompt |
Application-specific and prompt-parameter surfaces. |
A typical configuration is written before Update or after initialization when the application deliberately refreshes the UI state:
Pythia.Capabilities
.Endpoint(True)
.Thinking(True)
.ThinkingHigh(True)
.Vision(True)
.Integration(True)
.IntegrationFunction(True)
.Media(False)
.DeepResearch(False)
.Model(True)
.Update;The builder route is preferable when the configuration should be locked in code.
The capabilities JSON file is deliberately flat and readable:
{
"type": "setCapabilities",
"endpoint": true,
"thinking": true,
"vision": true,
"media": false,
"mediaCreateImage": false,
"mediaCreateVideo": false,
"deepResearch": false,
"integration": true,
"integrationFunction": true,
"model": true
}Editing JSON is convenient during integration and deployment. The type field must remain:
"setCapabilities"Otherwise the JS configuration message is ignored and the WebView keeps its previous state.
A card is the configuration unit for a user-selectable integration in Pythia-Webview2. Each card represents a tool or integration that the user can enable for a prompt: a function, an MCP server, a skill, an agent, or a custom application-specific feature.
- Five families, five JSON files
- JSON schema of a card file
- Manual editing of cards
- Selection cycle — from click to prompt
The TChatManagedItemKind enum lists the five families: function, mcp, skills, agents, custom. Each family corresponds to a dedicated file under the application support folder:
| File | Family | Role |
|---|---|---|
<ExeName>-function-cards.json |
Functions | Callable functions / function-calling schemas. |
<ExeName>-mcp-cards.json |
MCP | Available Model Context Protocol servers. |
<ExeName>-skill-cards.json |
Skills | Application skills. |
<ExeName>-agent-cards.json |
Agents | Pre-configured agents. |
<ExeName>-custom-cards.json |
Custom | Application-specific business integrations. |
Example location:
bin32\MyProject\support\MyProject-function-cards.json
All card files share the same shape:
{
"type": "card-selection-dialog-set-data",
"dialog": "function",
"cards": [
{
"id": "1EC2521C-9E0A-410B-8DD4-1D6997F9AFFF",
"name": "Get weather",
"commentaire": "Returns weather data for a given city",
"content": "{\"name\":\"get_weather\",\"description\":\"Get current weather\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}"
},
{
"id": "10B03BB4-B9D6-4D94-B181-0094434FEE9F",
"name": "Get local time",
"commentaire": "Returns the current local time",
"badge": "",
"content": "{\"name\":\"get_local_time\",\"description\":\"Get local time\",\"parameters\":{\"type\":\"object\"}}"
}
],
"selectedId": ""
}Field meaning:
| Field | Purpose |
|---|---|
id |
Unique identifier. A GUID is recommended. Used for selection routing. |
name |
Short label shown in the cards panel. |
commentaire |
Description shown as subtitle or tooltip. |
badge |
Optional small label such as Beta, Pro, or Local. |
content |
Free-form payload owned by the application/vendor service. |
selectedId |
Optional startup selection. |
dialog |
Family name. It must match the file family. |
content may contain JSON encoded as a string, an internal identifier, XML, or another application-specific payload. Pythia-Webview2 does not interpret this value.
To add a function card, duplicate an entry in the cards array, generate a new GUID for id, fill in name and commentaire, then populate content with the business payload expected by the vendor service. Save the file and restart the application.
The same method applies to MCPs, skills, agents, and custom cards.
When the user selects cards and submits a prompt, the selected identities arrive in the prompt state. For functions, they appear in:
TInputPromptState.Integration.&FunctionEach item contains identity data such as Id and Name:
for var Item in State.Integration.&Function do
begin
// Item.Id identifies the selected card.
// Item.Name is the label visible to the user.
end;The full content payload of the card is not necessarily carried inside TInputPromptState. Keep the prompt state lightweight and resolve Item.Id to the full card definition inside the service layer. The vendor service can then parse the matching content value and inject the resulting function schema, MCP manifest, skill descriptor, agent configuration, or custom payload into the vendor request.
The adapter can still intercept upstream selections through methods such as DoSelectFunctionItem, DoSelectMCPItem, DoSelectSkillItem, DoSelectAgentItem, and DoSelectCustomItem, for example when a Delphi dialog must be opened before returning an item.
The model selector has two distinct configuration levels:
- the list of models available in the application;
- the runtime category configuration that decides which operation categories are visible and which model is currently selected by default for each category.
A model can be used as a category default only if it is first declared in the model list.
- Declare the available models
- Configure visible categories and default models
- Avoid
No default model configuredwarnings - User-side correction in production
- Read the selected model in the vendor
The generated model list file is stored under the support folder:
bin32\MyProject\support\MyProject-model-list.json
or:
bin64\MyProject\support\MyProject-model-list.json
Example:
{
"type": "model-selector-set-data",
"models": [
{
"id": "anthropic-sonnet-example",
"label": "Claude Sonnet",
"capabilityLabels": ["Thinking", "Vision"],
"categoryId": "textGeneration"
},
{
"id": "image-model-example",
"label": "Image Model",
"capabilityLabels": ["Create Image", "Vision"],
"categoryId": "imageCreation"
},
{
"id": "deep-research-model-example",
"label": "Deep Research Model",
"capabilityLabels": ["Deep Research"],
"categoryId": "deepResearch"
}
],
"activeCategoryId": "textGeneration",
"selectedModelId": "anthropic-sonnet-example"
}Field meaning:
| Field | Purpose |
|---|---|
id |
UI-side identifier. It can be an alias or the exact vendor model name, depending on the adapter. |
label |
Human-readable label shown in the selector. |
capabilityLabels |
Tags displayed under the label. |
categoryId |
Category such as textGeneration, imageCreation, videoCreation, audioCreation, textToSpeech or deepResearch. |
Replace example id values with the exact identifiers expected by your vendor implementation, or keep stable aliases and map them inside the vendor service.
The second configuration level is the runtime model-selector category configuration, usually stored in:
bin32\MyProject\support\MyProject-model-get-replace-version.json
or:
bin64\MyProject\support\MyProject-model-get-replace-version.json
Despite the historical event name model-selector-get-replace-version, this file is not merely a version-alias table. It defines:
- which model categories are visible;
- which source category each category is linked to;
- which model is currently selected by default for each category.
Example:
{
"categoryConfigMode": "replace",
"categories": [
{
"id": "allModels",
"label": "Model list",
"badge": "",
"sourceCategoryId": "none",
"featureLabels": ["All"],
"model": "",
"visible": true
},
{
"id": "textGeneration",
"label": "Text Generation",
"badge": "",
"sourceCategoryId": "text",
"featureLabels": [
"Thinking",
"Attach files",
"Web research",
"Vision"
],
"model": "anthropic-sonnet-example",
"visible": true
},
{
"id": "imageCreation",
"label": "Image Creation",
"badge": "",
"sourceCategoryId": "image",
"featureLabels": [
"Create Image",
"Vision"
],
"model": "",
"visible": true
},
{
"id": "videoCreation",
"label": "Video Creation",
"sourceCategoryId": "video",
"featureLabels": ["Create Video"],
"model": "",
"visible": false
}
],
"type": "model-selector-set-runtime-config"
}The important field is:
"model": "anthropic-sonnet-example"The value must match the id of a model declared in <ExeName>-model-list.json.
A visible category with an empty model field means that the category is exposed in the UI, but no model has been selected for it yet:
{
"id": "imageCreation",
"label": "Image Creation",
"model": "",
"visible": true
}This can be intentional if the user should make the first selection from the model configuration panel. Once the user selects a model, that choice is persisted.
Warnings such as:
No default model: text generation cannot be processed
No default model configured: Image creation aborted
No default model configured: Video creation aborted
No default model configured: Audio creation aborted
No default model configured: TTS operation aborted
No default model configured: Deep Research operation aborted
mean that the requested operation category is visible or reachable, but no default model is currently assigned to it.
Before shipping, choose one of three strategies for every category.
| Strategy | Configuration | Result |
|---|---|---|
| Provide a default model | visible: true and model contains a valid model id |
The feature can be used immediately. |
| Let the user choose | visible: true and model is empty |
The first use may warn until the user selects a model. |
| Hide unsupported category | visible: false |
The feature is not exposed. |
If a category is not supported by the application, or if no matching model is provided, hide it:
{
"id": "videoCreation",
"label": "Video Creation",
"model": "",
"visible": false
}In production, the user should not edit JSON files manually.
If the user sees a warning such as:
No default model configured: Image creation aborted
it means that the category is visible, but no model is currently selected for it. The user-side procedure is:
- open the model configuration panel;
- look for categories marked as not assigned, usually with a red dot;
- select the relevant category, such as Text Generation, Image Creation, Video Creation, Audio Creation, Text to Speech, or Deep Research;
- choose one of the compatible models from the filtered list;
- retry the operation.
The developer decides which categories exist and are visible. The user only chooses or changes the current model for visible categories.
Inside the vendor service, read the selected model from the buffered state and convert it into the exact model identifier expected by the SDK.
A sample architecture may use an indexed category lookup:
State.Model := State.Models.Items[TEXT_GENERATION_INDEX].Model;Make sure the categoryId values in the model list and the category ids in the runtime configuration match what the vendor service expects. If the category mapping does not match, the vendor may read the wrong slot or fail to find a default model.
Pythia-Webview2 contains no LLM call. It delegates provider communication to an application service implementing IVendorServices. The service adapter receives the UI event, then routes prompt submission to that vendor service.
LLM-related processing must be asynchronous so the UI remains responsive. Network calls, streaming, and long-running generations must not be performed directly on the UI thread.
- The IVendorServices interface
- TInputPromptState and TStateBuffer
- TManagedItemFinalizeProc — the request end
- Route prompt submission to the vendor
- Vendor service skeleton
- Initialize the vendor after the browser is ready
- Streaming and UI
Declared in WVPythia.Vendors.Services.pas:
IVendorServices = interface
['{E7464431-EB15-4121-ADB4-76C40F1A9BEE}']
procedure AsyncAwaitStreamChat(
const AState: TInputPromptState;
const AOnFinalize: TManagedItemFinalizeProc);
procedure UpdateApiKey;
end;AsyncAwaitStreamChat is invoked on prompt submission. It receives the complete input state and a finalization callback. UpdateApiKey lets the SDK client reload credentials after key creation or rotation.
TInputPromptState aggregates the prompt text, endpoint, thinking configuration, files, images, knowledge selections, selected cards, media state, request parameters, and model selections.
For asynchronous vendor calls, do not capture TInputPromptState directly in long-lived closures. It is a class reference and may be freed by the component before a long streaming request finishes.
Copy it immediately into a value buffer:
var State := TStateBuffer.FromState(AState);Then capture State, not AState, inside asynchronous callbacks.
TManagedItemFinalizeProc = reference to procedure(
const AResult: TManagedItemLLMResult);At the end of the request — success, cancellation, or exception — the vendor service fills a TManagedItemLLMResult and invokes the callback:
var Result := TManagedItemLLMResult.New;
try
Result
.UsedModel(State.Model)
.Response(FinalText)
.Reasoning(FinalThinking)
.PromptJson(RequestJson)
.ResponseJson(ResponseJson)
.Error(False);
if Assigned(AOnFinalize) then
AOnFinalize(Result);
finally
Result.Free;
end;In the service unit, locate the method that handles prompt activation. A diagnostic implementation can be replaced with:
class function TToolContainer.ActivateInputState(
const AState: TInputPromptState;
const AOnFinalize: TManagedItemFinalizeProc): Boolean;
begin
Result := True;
AnthropicVendor.AsyncAwaitStreamChat(AState, AOnFinalize);
end;The adapter does not need to know the SDK. It only routes the prompt state toward the selected application service.
type
TAnthropicServices = class(TInterfacedObject, IVendorServices)
const
API_KEY_NAME = 'anthropic';
private
FBrowser: IPythiaBrowser;
FClient: IAnthropic;
public
constructor Create(const ABrowser: IPythiaBrowser);
procedure AsyncAwaitStreamChat(
const AState: TInputPromptState;
const AOnFinalize: TManagedItemFinalizeProc);
procedure UpdateApiKey;
end;
constructor TAnthropicServices.Create(const ABrowser: IPythiaBrowser);
var
Anthropic_key: string;
begin
FBrowser := ABrowser;
if not FBrowser.ApiKeySecretStore.ReadSecret(API_KEY_NAME, Anthropic_key) then
FBrowser.TryHandleAsCommand(Format('/api-key new %s', [API_KEY_NAME]));
FClient := TAnthropicFactory.CreateInstance(Anthropic_key);
end;
procedure TAnthropicServices.UpdateApiKey;
var
Anthropic_key: string;
begin
if not FBrowser.ApiKeySecretStore.ReadSecret(API_KEY_NAME, Anthropic_key) then
begin
FClient.API.Token := '';
Exit;
end;
FClient.API.Token := Anthropic_key;
FBrowser.DisplaySuccess('Anthropic client is up to date.');
end;
procedure TAnthropicServices.AsyncAwaitStreamChat(
const AState: TInputPromptState;
const AOnFinalize: TManagedItemFinalizeProc);
begin
var State := TStateBuffer.FromState(AState);
// 1. Build the request from State.
// 2. Resolve selected cards by Id if tools are enabled.
// 3. Start the SDK request asynchronously.
// 4. Stream chunks toward FBrowser.DisplayStream(...).
// 5. Invoke AOnFinalize with TManagedItemLLMResult at the end.
end;For another provider, keep the same structure and change the service class, SDK factory, model mapping, and API_KEY_NAME value.
Instantiate the vendor after the form is shown and the browser has had time to initialize. One simple pattern is to delay the initialization and create the vendor from DoInitialize:
uses
VCL.WVPythia.Chat,
WVPythia.Types,
VCL.WVPythia.Services,
Anthropic.Browser.Services;
procedure TForm1.FormCreate(Sender: TObject);
begin
Pythia := TVCLPythia.Create(Panel2);
Pythia.ServiceAdapter := TVCLChatManagedItemDialogService.Create;
Pythia.OnApiKeyChanged := UpdateApiKey;
Pythia.OnInitialized := DoOnInitialized;
Pythia.Update;
end;
procedure TForm1.DoInitialize;
begin
AlphaBlend := False;
AnthropicVendor := TAnthropicServices.Create(Pythia);
end;
procedure TForm1.UpdateApiKey(KeyName: string);
begin
if SameText(KeyName, TAnthropicServices.API_KEY_NAME) then
AnthropicVendor.UpdateApiKey;
end;The resulting flow is:
Application starts
│
▼
DoInitialize creates TAnthropicServices
│
▼
The service reads the secret named "anthropic"
│
├── Key exists: create the Anthropic client with the stored key
│
└── Key missing: trigger /api-key new anthropic automatically
│
▼
User enters the key
│
▼
OnApiKeyChanged calls UpdateApiKey
│
▼
The Anthropic client receives the new token
During the stream, the vendor service pushes chunks to the WebView through:
IPythiaBrowser.DisplayStream(TextDelta, ThinkingDelta, IsFinal)The component accumulates fragments and renders them in real time inside the current chat bubble. The final callback closes the sequence and triggers persistence of the complete turn.
User cancellation is funneled through FBrowser.Escape: Boolean. The vendor service should poll this value in its progress callback and stop cleanly when it becomes True. After cancellation, reset the relevant browser state so the UI is released.
Pythia-Webview2 natively renders media outputs produced by an LLM: images, audios, videos, files. Rendering relies on dedicated JS templates (DisplayImageTemplate.js, DisplayAudioTemplate.js, DisplayVideoTemplate.js, DisplayFileTemplate.js) and on the WVPythia.Net.MediaCodec.pas utility unit on the Delphi side.
The vendor service fills the Images, Audios, Videos, Files arrays of TManagedItemLLMResult with strings that may take three forms: a local file path (C:\path\to\image.png), an HTTP or HTTPS URL (https://...), or a complete data URI (data:image/png;base64,...). The component detects the format at render time and adapts its strategy: a local file is encoded into a data URI on the fly via TMediaCodec.ToDataURI, an HTTP URL is inserted as is in the matching tag, a data URI is used directly without transformation.
The WVPythia.Net.MediaCodec.pas unit exposes a TMediaCodec helper record with a complete suite of static methods to encode and decode Base64 and data URIs. The most useful ones in a vendor service are EncodeDataUri(FilePath, MimeType) which produces data:<mime>;base64,... from a file; TryDecodeDataUriToFile(DataUri, FilePath, out MimeType) which saves a data URI into a file on disk; TryToBytes(FileLocation, out Bytes, out MimeType) which resolves any source — file, URL, data URI — into bytes; TryToDataUri(FileLocation, out DataUri, out MimeType) which converts any source into a data URI; and GetMimeType(FileLocation) which returns the MIME type of a local file (by extension) or of a remote URL (HTTP HEAD).
Files returned by the vendor that point to a local resource exposed through the WebView2 virtual mapping use the https://app.local/... prefix. When TInputPromptState.ToImageSources encounters this prefix, it leaves the URL as is; otherwise it converts to a data URI. This distinction lets you serve large files from disk without paying the cost of Base64 conversion at every render — typically useful for generated videos that may reach several tens of megabytes.
For a media family to be visible in the UI, the matching capability must be active in <ExeName>-capabilities.json: mediaCreateImage, mediaCreateVideo, mediaCreateAudio, mediaTextToSpeech. Without these flags, the UI hides the toggles in the input bar and the vendor never receives any media-generation state to handle.
Current state. This version of Pythia-Webview2 does not offer live audio recording. The supported flow consists of providing an existing audio file that the application code then transcribes before pushing the result into the input bar. The microphone icon in the bar — enabled via ebMicrophone — is present in the UI but no default handler is associated with it: it is entirely the developer's responsibility to wire the desired logic.
A single line in FormCreate adds the icon to the input bar:
Pythia.EnabledButtons := Pythia.EnabledButtons + [ebMicrophone];The mediaSpeechToText capability must also be active in <ExeName>-capabilities.json. Without it, the icon stays hidden even if ebMicrophone is in the set.
When the user clicks the icon, the WebView emits audio-input and TBrowserEventManager routes the event to the adapter's DoActivateAudioInputEvent. The application is then free to define the behavior: open an audio-file selection dialog (.wav, .mp3, .m4a…), call an external transcription service, then inject the resulting transcription into the input bar via IPythiaBrowser.BrowserInput(transcription).
Direct recording from the system microphone is not provided by the framework in this version. It can be implemented by the host application if needed, for example through a third-party Delphi audio-capture component.
Audio transcription uses the Audio category in the model selector.
This category is used for models that can process an audio input and return text.
For example, a transcription model can be declared in <ExeName>-model-list.json like this:
{
"id": "WHISPER_LARGE",
"label": "Whisper large-v3",
"capabilityLabels": ["Speech to text"],
"categoryId": "speechToText"
}To make transcription available immediately, assign a default model to the Audio category in -model-get-replace-version.json:
{
"id": "speechToText",
"label": "Speech to Text",
"sourceCategoryId": "speech",
"model": "WHISPER_LARGE",
"visible": true
}If no default model is assigned to the Audio category, the user may see:
No default model configured: Audio creation aborted
The user can fix this from the model configuration panel by selecting a model for the Audio category.
The user can attach files to a prompt through the paperclip icon in the input bar. The file drawer displays the selected pieces before submission; on send, they are forwarded to the vendor service in TInputPromptState.Files, Images, or KnowledgeSearch, depending on the chosen category.
Clicking the paperclip emits the open-file-dialog event, accompanied by a target field that indicates the expected category (Images, Documents, Knowledge, Speech). TBrowserEventManager routes to IOpenDialog.SelectFile(target). The default implementation, provided by VCL.WVPythia.OpenDialog.pas or FMX.WVPythia.OpenDialog.pas depending on the framework, opens a system file-selection dialog. The resulting path is sent back to the WebView via IPythiaBrowser.UpdateFileDrawer(Files, Images, Knowledge), which refreshes the drawer display. At prompt submission, files are serialized into TInputPromptState according to their category: images go into Images, documents (.pdf, .docx, .txt) go into Files, files intended for retrieval (RAG) go into KnowledgeSearch. The vendor then decides how to forward them to the LLM — typically through TMediaCodec.EncodeDataUri for vendors that accept data URIs, or via a prior S3 upload for large files that would exceed payload limits.
The TOpenFileTarget enum (WVPythia.Types.pas) lists the four supported categories: Images, Documents, Knowledge, Speech. Each category applies a different extension filter in the open dialog. To customize a filter — for example to allow .csv in Knowledge — provide a custom implementation of IOpenDialog and inject it into the component via the event pipeline.
The Files, Vision, and KnowledgeSearch capabilities must be active in <ExeName>-capabilities.json for the corresponding attachment buttons to be visible. Without these flags, the paperclip is hidden for the affected category and the user cannot attach a file of that nature.
Pythia-Webview2 ships two native themes defined by the TLookAndFeel enum (WVPythia.Types.pas):
TLookAndFeel = (light, dark);The user selects the theme through the settings panel. The component emits the look-and-feel-selected event, applies the theme on the WebView side by updating root CSS variables, then calls the OnThemeChanged hook on the Delphi side. The current theme is readable through the property Pythia.Theme: string, parsable into an enum via TLookAndFeel.Parse.
The OnThemeChanged hook is where the host application aligns its chrome — window colors, toolbars, icons — with the chat theme. Without this synchronization, the user may end up with a dark chat framed by a light window, which breaks immersion:
Pythia.OnThemeChanged :=
procedure
begin
case TLookAndFeel.Parse(Pythia.Theme) of
TLookAndFeel.light: ApplyLightTheme;
TLookAndFeel.dark: ApplyDarkTheme;
end;
end;ApplyLightTheme and ApplyDarkTheme are to be written according to your host application's conventions: VCL Themes via TStyleManager, FireMonkey palette through the StyleBook property, direct chrome colors…
The CSS variables and classes used by the JS templates are defined in assets/index.htm and the files of assets/scripts/*.js. To modify the chat's visual appearance — palette, typography, spacing, border radius — you simply edit those files directly. Combined with TemplateAllwaysReloading (see §11), changes are visible without recompilation: edit a CSS or JS file, save, restart the application, see the result. To go further and add a third theme, you must extend the TLookAndFeel enum on the Pascal side and add the matching set of CSS variables on the JS side, respecting the --theme-<name>-... naming to stay consistent with the conventions of the shipped templates.
Several destructive actions — deleting a message, deleting a chat session — go through a confirmation dialog before being executed. The pattern relies on two distinct events that must not be conflated, on pain of an infinite loop or silent deletion without confirmation.
A naive implementation that executes the deletion as soon as the request event is received falls into one of two classic traps. Either the dialog reopens indefinitely because the deletion re-emits the initial event. Or the deletion happens without user validation, because the "request" was confused with the "response". The framework therefore enforces a strict separation between request and response.
On reception of the request — ConfirmationRequest emitted by the UI when the user clicks the trash icon — the Delphi handler simply records the pending action (for example, the ID of the message to delete) in a state variable. It executes nothing. The UI itself opens the confirmation dialog. When the user clicks "Confirm" in that dialog, the UI emits a second event, DialogConfirmationResponse. Only this second event triggers the actual execution of the action recorded in the previous step.
type
TPendingAction = record
Goal: TDialogGoal; // DeleteDomBlock | DeleteChatSession
TargetId: string;
end;
var
FPendingAction: TPendingAction;
// Step 1 — Recording only, NO execution.
procedure HandleConfirmationRequest(const Goal: TDialogGoal; const Id: string);
begin
FPendingAction.Goal := Goal;
FPendingAction.TargetId := Id;
// The UI opens the dialog; no execution here.
end;
// Step 2 — Conditional execution of the pending action.
procedure HandleDialogConfirmationResponse(const Confirmed: Boolean);
begin
if not Confirmed then
Exit;
case FPendingAction.Goal of
TDialogGoal.DeleteDomBlock: DeleteMessage(FPendingAction.TargetId);
TDialogGoal.DeleteChatSession: DeleteSession(FPendingAction.TargetId);
end;
FPendingAction := Default(TPendingAction);
end;The TDialogGoal enum (WVPythia.Types.pas) lists the two natively supported goals: DeleteDomBlock for deleting a message in the DOM and DeleteChatSession for deleting an entire conversation. For any additional application goal, simply extend the enum and handle the new value in both steps of the pattern. Resetting FPendingAction after execution avoids accidental executions if the user clicks Confirm again in another context.
Sessions form the chat's continuity layer: they retain the history of turns, allow returning to an existing conversation, and keep the display synchronized with the JSON store. The event manager activates, renames, paginates, or prepares deletion through IPersistentChat and IPythiaBrowser, so that the UI and persistence stay aligned.
Provided by WVPythia.ChatSession.Controller.pas.
TChatSessionaggregatesTChatTurnobjects.- Incremental JSON persistence via
WVPythia.JSON.Resource.pas. - Every structural mutation triggers a write.
- Pagination handled by
TChatSessionEventHandler.
Exact names in TBrowserChatEvent (WVPythia.Types.pas):
| Delphi name | Wire (JSON) |
|---|---|
NewChatEvent |
new-chat |
ChatSelectionEvent |
chat-selection |
ChatNextPageEvent |
chat-next-page |
ChatItemDeleteEvent |
chat-item-delete |
ChatItemRenameEvent |
chat-item-rename |
In the current flow, new-chat only creates a session if a conversation already contains at least one prompt and the browser is not locked. Selection loads the persisted session and redraws the conversation, pagination reloads a list page before refreshing the file drawer, and session deletion goes through a confirmation before mutating the store.
Deletion relies on alignment between the UI and the persistent state. Never delete on the persistence side without notifying the UI in the same turn — under penalty of a desync between the list shown in the DOM and the JSON store.
A command plugin (§12) executes Delphi code. As long as the action is limited to returning a text message via TCommandExecResult.Ok(...), everything happens on the Pascal side. But as soon as the command needs to display something specific, offer a choice, gather a structured input, two paths open up.
- Two ways to interact with the user
- The JS → Delphi bridge
- JSON contract
- Building a JS template for your plugin
- Delphi side: receiving and routing
- Channel limits
Option A — Drive the UI from Delphi.
The plugin calls methods of IPythiaBrowser (WVPythia.Chat.Interfaces.pas) to display a message, open a dialog, or inject a script. All the logic stays in DoExecute and the Delphi handlers. This is the fastest path when the existing UI covers the need — no JavaScript to write.
Option B — Write your own JS template.
If the command needs a rich display (custom form, visualization, interactive component), the plugin author ships a dedicated JS template. That template is loaded via LoadCustomTemplate (§11). User interactions inside the template — clicks, validations, end of animation — must travel up to the plugin's Delphi code. That is the role of the custom-event channel.
WebView2 offers only one path to talk to Delphi:
window.chrome.webview.postMessage(json);Pythia-Webview2 reserves the value "event": "custom-event" for user-defined messages. These messages are not routed to a typed framework handler: they arrive raw in CustomEvent and are forwarded to:
IChatManagedItemDialogService.ActivateCustomEvent(RawJson)
Exact signature: function ActivateCustomEvent(const ARawJson: string): Boolean; (WVPythia.Adapter.pas). It is up to the application to parse RawJson and dispatch on the name field.
{
"event": "custom-event",
"name": "<free, namespaced name>",
"payload": { },
"requestId": "<uuid, optional>"
}The framework looks at neither name nor payload. The only mandatory field it interprets is event, which must be literally "custom-event".
Two rules to follow.
1. Wrap in an IIFE.
Each template is written as a self-executing function that does not pollute the global window namespace:
(() => {
// local state, DOM, event listeners…
})();Variables, functions, and listeners declared inside stay confined to the closure. This is the convention followed by the 22 shipped templates; it is necessary for several templates to coexist without name collisions.
2. Return JSON to Delphi.
Every notification toward the Pascal code — user click, form validation, data request — goes through a postMessage call respecting the custom-event contract:
(() => {
const btn = document.getElementById('my-plugin-confirm');
btn.addEventListener('click', () => {
window.chrome.webview.postMessage({
event: "custom-event",
name: "myplugin.run-confirmed",
payload: { id: 42, label: "Option A" }
});
});
})();To avoid repeating the structure in each template, inject a helper once from a bootstrap template:
window.AppHost = Object.freeze({
emit(name, payload = {}, requestId = crypto.randomUUID()) {
window.chrome.webview.postMessage({
event: "custom-event",
name, payload, requestId
});
return requestId;
}
});Usage from a plugin template:
(() => {
document.getElementById('my-plugin-confirm')
.addEventListener('click', () => {
AppHost.emit("myplugin.run-confirmed", { id: 42 });
});
})();ActivateCustomEvent receives the raw message. The application parses it and dispatches by name:
function TMyDialogService.ActivateCustomEvent(const ARawJson: string): Boolean;
begin
var Reader := TJsonReader.Parse(ARawJson);
var EventName := Reader['name'].AsString;
if EventName = 'myplugin.run-confirmed' then
begin
var Id := Reader['payload.id'].AsInteger;
FMyPluginController.HandleRunConfirmed(Id);
Exit(True);
end;
Result := False; // not consumed; lets other consumers take over
end;name-based routing belongs to the application. Recommendation: namespace event names (myplugin.run-confirmed, user.form-validated) to avoid collisions between plugins that coexist.
- No automatic typed binding — always go through
TJsonReader.Parse. - One-way channel by construction. For a structured response on the JS side, combine
requestId+ anExecuteScriptcall from Delphi that triggers a registered JS callback. - No sandbox: all templates share the same WebView context. A template can read/write the DOM produced by another.
- Scripts injected via
ExecuteScriptexecute in the same global scope as the templates — beware of exported function names.
Pythia-Webview2 ships ready-to-use locale files under assets/lang/. Each JSON file describes the strings displayed by the interface, grouped by logical sections such as more, settings, and dialogs. The mechanism covers both the JavaScript layer — which queries labels through a t('key') API — and the Pascal layer, where strings are stored in global variables reloaded on every language switch.
assets/lang/*.json
A few standard locales:
english-usenglish-gbfrench-frjapan-jp
Loading is driven by the VCL or FMX browser language manager. Both expose the same conceptual surface: SetLanguage to activate a locale, GetLocalLanguage to query the current locale, LoadDictionaryContent to retrieve the content of a dictionary, and GetNormalizedFileNames to enumerate available locales.
Templates access labels through:
window.AppI18n.t('key')The API is exposed by BootstrapDictionaryTemplate.js, which loads the active dictionary at WebView startup and updates it on every SetLanguage call.
Native strings are defined as global variables in WVPythia.Strs.pas. They are reloaded on every language switch: the JSON dictionary is parsed, each known key updates its corresponding global variable, and the current value is kept as fallback when a key is missing.
Duplicate an existing .json file, translate its values, and name the new file following the schema:
<country>-<code>.json
Once the file is dropped into assets/lang/, it is detected on next startup. If the application has already been run, the selected language may be remembered in <ExeName>-request-params-main-values.json; deleting that preference file forces the default language to be reinitialized on next startup.
The S_... variables of WVPythia.Strs.pas cover the component's native strings. A host application can add its own labels — custom plugin captions, business assistant messages, additional button titles — by using the same dictionary-loading cycle.
At the root of every language file, add a sub-object by convention named custom:
{
"more": {
"delete_qa": "...",
"welcome": "..."
},
"settings": {
"...": "..."
},
"custom": {
"my_dashboard_title": "Dashboard",
"my_run_button": "Run",
"myplugin_confirm_dialog": "Confirm action?"
}
}The framework does not interpret this object. It is a grouping convention for host-application strings.
unit MyApp.Strs;
interface
var
S_MY_DASHBOARD_TITLE: string = 'Dashboard';
S_MY_RUN_BUTTON: string = 'Run';
S_MYPLUGIN_CONFIRM_DIALOG: string = 'Confirm action?';
implementation
end.The initial values serve as fallbacks.
procedure MyTranslation;
begin
{--- Retrieves the content of the active dictionary via the component }
var Content := Pythia.LoadDictionaryContent(
Pythia.GetLocalLanguage);
var JSONObject := TJsonReader.Parse(Content);
S_MY_DASHBOARD_TITLE := JSONObject.AsString(
'custom.my_dashboard_title',
S_MY_DASHBOARD_TITLE);
S_MY_RUN_BUTTON := JSONObject.AsString(
'custom.my_run_button',
S_MY_RUN_BUTTON);
S_MYPLUGIN_CONFIRM_DIALOG := JSONObject.AsString(
'custom.myplugin_confirm_dialog',
S_MYPLUGIN_CONFIRM_DIALOG);
end;Assign OnTranslationsLoaded during component instantiation. The examples below are partial snippets; in a real application, keep the central rule from §7 and §8: assign ServiceAdapter before calling Update.
{--- VCL }
Pythia := TVCLPythia.Create(Panel2);
Pythia.ServiceAdapter := TVCLChatManagedItemDialogService.Create;
Pythia.OnTranslationsLoaded := MyTranslation;
Pythia.Update;{--- FMX }
Pythia := TFMXPythia.Create(Layout1);
Pythia.AttachHost(Self);
Pythia.ServiceAdapter := TFMXChatManagedItemDialogService.Create;
Pythia.OnTranslationsLoaded := MyTranslation;
Pythia.Update;From that moment on, every call to SetLanguage retranslates both native strings and the host application's custom strings.
On first launch, Pythia-Webview2 creates application configuration files derived from the executable name. The files most often edited by the integrator live in the application support folder:
bin32\MyProject\support
or:
bin64\MyProject\support
A standalone project usually has this shape:
MyProject
├── assets
│ ├── index.htm
│ ├── scripts
│ ├── media
│ └── lang
├── bin32
│ ├── MyProject.exe
│ ├── WebView2Loader.dll
│ └── MyProject
│ └── support
├── bin64
│ ├── MyProject.exe
│ ├── WebView2Loader.dll
│ └── MyProject
│ └── support
└── dcu
The exact set of generated files may vary with the project version and enabled features. The files below are the integration-critical ones covered by this documentation.
| File | Role | Manually editable |
|---|---|---|
<ExeName>-capabilities.json |
Declares which UI features are visible or available. | Yes — useful during integration and deployment. |
<ExeName>-model-list.json |
Declares the models available to the selector. | Yes — add, remove, or rename available models. |
<ExeName>-model-get-replace-version.json |
Runtime model-selector category configuration: visible categories, source categories, and default model per category. | Yes — assign default models or hide unsupported categories. |
<ExeName>-function-cards.json |
Function-card definitions. | Yes — declare function-calling tools. |
<ExeName>-mcp-cards.json |
MCP-card definitions. | Yes — declare MCP integrations. |
<ExeName>-skill-cards.json |
Skill-card definitions. | Yes — declare skills. |
<ExeName>-agent-cards.json |
Agent-card definitions. | Yes — declare agents. |
<ExeName>-custom-cards.json |
Application-specific cards. | Yes — free extension point for business integrations. |
<ExeName>-custom-template-js.json |
References to custom JS templates loaded via LoadCustomTemplate, when this feature is used. |
Yes — keep consistent with files under assets/scripts. |
<ExeName>-capabilities.json is a flat setCapabilities payload:
{
"type": "setCapabilities",
"endpoint": true,
"thinking": true,
"vision": true,
"media": false,
"deepResearch": false,
"integration": true,
"integrationFunction": true,
"model": true
}<ExeName>-model-list.json declares available models:
{
"type": "model-selector-set-data",
"models": [
{
"id": "anthropic-sonnet-example",
"label": "Claude Sonnet",
"capabilityLabels": ["Thinking", "Vision"],
"categoryId": "textGeneration"
}
],
"activeCategoryId": "textGeneration",
"selectedModelId": "anthropic-sonnet-example"
}<ExeName>-model-get-replace-version.json configures runtime model categories:
{
"categoryConfigMode": "replace",
"categories": [
{
"id": "textGeneration",
"label": "Text Generation",
"sourceCategoryId": "text",
"featureLabels": ["Thinking", "Vision"],
"model": "anthropic-sonnet-example",
"visible": true
},
{
"id": "videoCreation",
"label": "Video Creation",
"sourceCategoryId": "video",
"featureLabels": ["Create Video"],
"model": "",
"visible": false
}
],
"type": "model-selector-set-runtime-config"
}A visible category should either have a valid model value or be intentionally left for user assignment. Unsupported categories should be hidden.
Card files share the same root shape:
{
"type": "card-selection-dialog-set-data",
"dialog": "function",
"cards": [],
"selectedId": ""
}The dialog value must match the family represented by the file.
Other files may be created near the executable or under the application folder, such as request-parameter values, chat-session history, API-key name tracking, or developer-mode exchange dumps. Their exact location and shape can depend on project configuration and version. When a file is not covered by the sections above, treat its format as version-specific unless the corresponding source unit documents it explicitly.
The table below gathers the main contracts to know in order to integrate the component. These interfaces are the stable entry points between the browser, commands, application services, and configuration providers; the concrete FMX/VCL classes consume them without exposing their internal details.
| Interface | Unit | Role |
|---|---|---|
IPythiaBrowser |
WVPythia.Chat.Interfaces.pas |
Component façade: ExecuteScript, DisplayError, ApiKeyValuesUpdate, etc. — see the full interface lines 49-238 |
ICommandPlugin / ICommandRegistry |
WVPythia.Chat.Interfaces.pas |
Command pipeline |
IApiKeyService |
WVPythia.ApiKey.Service.Intf.pas |
CreateKey / DeleteKey / Exists |
ISecretStore |
WVPythia.Chat.Interfaces.pas |
Windows-Registry abstraction |
IChatManagedItemDialogService |
WVPythia.Adapter.pas |
Neutral UI contract for selection / activation, including ActivateCustomEvent |
IOpenDialog / IProcessExecute |
WVPythia.Chat.Interfaces.pas |
Files and external processes |
ICapabilities |
WVPythia.Capabilities.Manager.pas |
Fluent capability builder: endpoints, media, thinking, MCP, skills, agents |
ITemplateProvider |
WVPythia.Template.Manager.pas |
JS template loading, LoadCustomTemplate, reload strategies |
Enumeration: WVPythia.Types.pas — 38 values, mapped to wire names via TBrowserChatEventHelper.Map (WVPythia.Types.pas).
- Prompt & orchestration
- Chat sessions
- Message actions
- Integration dialogs
- Model selector
- Card selector
- Settings & appearance
- Extension channel
| Delphi | Wire |
|---|---|
InputSubmit |
input-submit |
InputState |
input-state |
InputString |
— |
StopSubmit |
stop-submit |
AudioInput |
audio-input |
| Delphi | Wire |
|---|---|
NewChatEvent |
new-chat |
ChatSelectionEvent |
chat-selection |
ChatNextPageEvent |
chat-next-page |
ChatItemDeleteEvent |
chat-item-delete |
ChatItemRenameEvent |
chat-item-rename |
| Delphi | Wire |
|---|---|
&Copy |
copy |
CopyEvent |
copy-event |
BranchEvent |
branch-event |
DeleteEvent |
delete-event |
ScrollRequest |
scroll-request |
| Delphi | Wire |
|---|---|
OpenFileDialog |
open-file-dialog |
OpenIntegrationFunctionDialog |
open-integration-function-dialog |
OpenIntegrationMcpDialog |
open-integration-mcp-dialog |
OpenIntegrationSkillsDialog |
open-integration-skills-dialog |
OpenIntegrationAgentsDialog |
open-integration-agents-dialog |
OpenCustomDialog |
open-custom-dialog |
DisplayFileClick |
display-file-click |
DialogConfirmationResponse |
dialog-confirmation-response |
| Delphi | Wire |
|---|---|
ModelSelection |
model-selection |
ModelSelectorCategoryChanged |
model-selector-category-changed |
ModelSelectorSelectionChanged |
model-selector-selection-changed |
ModelSelectorGetReplaceVersion |
model-selector-get-replace-version |
| Delphi | Wire |
|---|---|
CardSelectionDialogSettings |
card-selection-dialog-settings |
CardSelectionDialogSelect |
card-selection-dialog-select |
CardSelectionDialogSelectionChanged |
card-selection-dialog-selection-changed |
CardSelectionDialogCancel |
card-selection-dialog-cancel |
| Delphi | Wire |
|---|---|
SystemSettings |
system-settings |
RequestParamsValues |
request-params-values |
ResquestParamsPageChanged |
resquest-params-page-changed (original typo preserved in the enum) |
LookAndFeelSelectedEvent |
look-and-feel-selected |
LanguageSelectedEvent |
language-selected |
ScrollButtonSelectedEvent |
scroll-button-selected |
| Delphi | Wire |
|---|---|
CustomEvent |
custom-event |
| Element | VCL | FMX |
|---|---|---|
| Root component | TVCLPythia |
TFMXPythia |
| WebView host | Direct via WebView4Delphi VCL | uWVFMXHost.pas + uWVFMXCoreInit.pas |
| OpenDialog | VCL.WVPythia.OpenDialog.pas |
FMX.WVPythia.OpenDialog.pas |
| Language manager | TVCLPythiaLanguageManager |
TFMXPythiaLanguageManager |
| Public surface | Identical | Identical |
| Demo | demos/VCL/new-projet, demos/VCL/pythia-sample, demos/VCL/pythia-anthropic |
demos/FMX/new-projet, demos/FMX/plugin-git, demos/FMX/plugin-snippet |
The TTemplateType enumeration (WVPythia.Template.Manager.pas) associates each template with a file under assets/scripts/. The table below is the complete reference, ordered by enum value, and is the starting point for any substitution via LoadCustomTemplate (see §11) — the developer must locate the template to override starting from the UI area they want to modify.
| Enum | File | Role |
|---|---|---|
main_html |
index.htm |
Root HTML skeleton; includes DOM containers and loads injected scripts |
js_response |
DisplayTemplate.js |
Rendering of an assistant turn (markdown, code, citations) |
js_prompt |
PromptTemplate.js |
Rendering of a user turn |
js_waitfor |
ReasoningTemplate.js |
Animation and display of the reasoning (thinking) block |
js_inputbubble |
InputBubbleTemplate.js |
Input bar: textarea, buttons, submit/stop state |
js_scrollButtons |
ScrollButtonsTemplate.js |
Up/down scroll buttons |
js_images |
DisplayImageTemplate.js |
Rendering of generated images |
js_promptFile |
PromptFileTemplate.js |
Display of attachments inside the user turn |
js_audio |
DisplayAudioTemplate.js |
Inline audio player |
js_video |
DisplayVideoTemplate.js |
Inline video player |
js_displayfile |
DisplayFileTemplate.js |
Clickable file card inside the assistant turn |
js_selector |
SelectorTemplate.js |
Card selector (functions, MCP, skills, agents) |
js_confirmationDialog |
ConfirmationDialogTemplate.js |
Two-step confirmation dialog (see §21) |
js_filesMenager |
FilesDrawerTemplate.js |
Drawer of files attached to the input bar |
js_errors |
ErrorsTemplate.js |
Rendering of error messages |
js_requestParams |
RequestParamsTemplate.js |
Settings panel (temperature, top-p, system prompt…) |
js_bootstrapDictionary |
BootstrapDictionaryTemplate.js |
i18n dictionary and window.AppI18n.t helper |
js_models |
ModelsTemplate.js |
Model selector |
js_chatFooter |
ChatFooterTemplate.js |
Chat footer (icons, statuses) |
js_cardSelector |
CardSelectorTemplate.js |
Multi-card selection dialog |
js_promptSummary |
PromptSummaryTemplate.js |
Compact summary of a submitted prompt |
js_inputDialog |
InputDialogTemplate.js |
Modal input box (for example /api-key new) |
js_injectionEnded |
InjectionEndedTemplate.js |
End-of-injection signal for JS |
23 entries in total: one HTML root and 22 JavaScript fragments.
The DEV_MODE conditional define enables diagnostic output useful during integration.
In:
Project → Options → Compilation → Conditional defines
add:
DEV_MODE
Then rebuild completely, not incrementally.
With DEV_MODE enabled, Pythia-Webview2 writes the last JSON message received from WebView2 to a debug exchange file near the executable or the application support path, depending on the project configuration. The file is overwritten on each message and is intended for direct observation of the payload that triggered the last handler.
Use this file when a handler behaves unexpectedly. It shows the exact JSON shape received by the Delphi event pipeline.
During integration, keep a way to inspect TInputPromptState received from the UI. A diagnostic handler can render the raw prompt-state JSON in the chat area:
class function TToolContainer.ActivateInputState(
const AState: TInputPromptState;
const AOnFinalize: TManagedItemFinalizeProc): Boolean;
begin
Result := True;
var LReader := TJsonReader.Parse(AState.Source);
var JsonContent := TEscapeHelper.ToPreformattedHTML(LReader.Format());
Form1.Pythia.Display(JsonContent, False);
var Returns := TManagedItemLLMResult.Create;
try
Returns
.UsedModel('diagnostic')
.Response(JsonContent)
.Error(False);
if Assigned(AOnFinalize) then
AOnFinalize(Returns);
finally
Returns.Free;
end;
end;This is the fastest way to verify selected model, selected cards, endpoint options, capabilities-related state, prompt text, and attached context.
Right-click in the chat area and choose Inspect. Use:
- Console for JavaScript errors;
- Network for resource loading;
- Elements for DOM and CSS inspection.
This is especially useful when customizing JavaScript templates, CSS, or asset loading.
If the UI does not load correctly, first check:
Updatewas called;assets/is at the expected level relative tobin32andbin64;WebView2Loader.dllis next to the executable and matches the target architecture;- the support folder being edited is the active one.
A deployed Pythia-Webview2 application needs more than the executable. It must ship the matching WebView2 loader, the complete assets folder, and a predictable support-folder layout.
- Deployment manifest
- Pre-populate the configuration
- WebView2 runtime
- First-launch behavior
- Windows x86 vs x64 distribution
For a standalone project, the expected structure is:
MyFirstPythia-Webview2
├── assets
│ ├── index.htm
│ ├── scripts
│ ├── media
│ └── lang
├── bin32
│ ├── MyFirstPythia-Webview2.exe
│ └── WebView2Loader.dll
├── bin64
│ ├── MyFirstPythia-Webview2.exe
│ └── WebView2Loader.dll
├── dcu
├── Main.pas
├── Main.dfm or Main.fmx
├── VCL.WVPythia.Services.pas
│ or
├── FMX.WVPythia.Services.pas
└── MyFirstPythia-Webview2.dproj
On first launch, Pythia-Webview2 creates the application support folder, for example:
bin32\MyFirstPythia-Webview2\support
or:
bin64\MyFirstPythia-Webview2\support
This folder contains generated or pre-filled JSON configuration files: capabilities, model list, runtime category configuration, cards, and other support files.
If the application ships with curated defaults, pre-fill the support folder before deployment. Typical files include:
<ExeName>-capabilities.json;<ExeName>-model-list.json;<ExeName>-model-get-replace-version.json;- card files such as
<ExeName>-function-cards.json; - other support files used by the application.
If these files already exist on first launch, Pythia-Webview2 can use them instead of generating default configuration files.
There are two separate elements:
WebView2Loader.dll
and the Microsoft Edge WebView2 Runtime.
WebView2Loader.dll is the DLL loaded by your application. It must sit next to the executable and match the architecture: use the bin32 DLL for Win32 and the bin64 DLL for Win64.
The WebView2 Runtime is the Microsoft Edge-based runtime installed on the target machine. Recent Windows installations often already have it, but clean or enterprise machines may require deployment.
The Evergreen Runtime is the recommended deployment mode for most applications because it is installed on the machine and updated automatically by Microsoft.
Microsoft provides:
- Evergreen Bootstrapper;
- Evergreen Standalone Installer;
- x86, x64, and ARM64 installers.
The bootstrapper downloads and installs the appropriate runtime for the machine. The standalone installer is better for offline environments or enterprise deployment.
On first launch, WebView2 may create a per-user profile under the user's local application data. This is normal browser-runtime behavior.
For kiosk or disposable-session scenarios, decide explicitly whether the WebView2 user data folder should be preserved or cleaned at startup.
Binaries are produced under bin32 or bin64 depending on the target. Choose x64 by default unless a project constraint requires Win32. Always ship the matching WebView2Loader.dll.
| Term | Definition |
|---|---|
| Adapter | Application class implementing IChatManagedItemDialogService, which acts as a bridge between UI events and business logic (see §8 adapter sub-section). |
| Card | Configuration unit for an integration (function, MCP, skill, agent, custom). Stored in a JSON file under <ExeName>/support/; displayed by the UI in the Cards panel (see §14). |
| Capabilities | Boolean configuration of features exposed by the application (endpoints, media, integrations…). Driven by ICapabilities, persisted in <ExeName>-capabilities.json (see §13). |
| Custom event | JSON message emitted by a JS template through postMessage with event: "custom-event". Received on the Delphi side by IChatManagedItemDialogService.ActivateCustomEvent (see §23). |
| Host | Delphi application that embeds the Pythia-Webview2 component. |
| Managed item | Item selectable from the UI: a card or an integration option. Represented by TChatManagedItemRef (Id + Name) when returned from a selection. |
| Plugin (command) | Class implementing a slash command. Inherits from TCommandPlugin, wired via Pythia.OnRegisterCommandPlugins (see §12). |
| Prompt state | Snapshot of the input bar at submission time. Type: TInputPromptState. Received by IVendorServices.AsyncAwaitStreamChat (see §16). |
| Template | JS fragment injected into the WebView to render a portion of the UI. 22 templates shipped; each can be overridden via LoadCustomTemplate (see §11 and §29). |
| Turn | A user prompt / assistant response pair in a chat session. Type: TChatTurn. |
| Vendor | Application service implementing IVendorServices, which actually talks to the LLM API (Anthropic, OpenAI, etc.). Wired through the adapter (see §16). |
| WebView (host) | TWVBrowser component on the VCL side, or the TWVFMXBrowser + TWVFMXHost + TWVFMXCoreInit stack on the FMX side, hosting the WebView2 runtime. |
Pythia-Webview2 is designed as a trusted local WebView2 client controlled by the host Delphi application. It is not a general-purpose browser, not a sandbox for arbitrary web content, and not a secret-management product. Its security model is intentionally simple: local UI assets are rendered in WebView2, events are routed back to Delphi through explicit contracts, and application-specific security decisions remain under the integrator's control.
The default posture is suitable for a local desktop application, provided the host application keeps the embedded UI constrained and validates any data that crosses the JavaScript ↔ Delphi boundary.
- Secret storage
- Embedded WebView2 surface
- HTML, Markdown, and escaping
- WebView2 permissions
- Custom events
- Custom templates
- JSON and threading
- Scope of responsibility
API key values are stored through ISecretStore. The default implementation stores secrets in the current user's Windows Registry.
By default, these values benefit from the operating system's user-level isolation: they are protected by the current Windows user context. No additional cryptographic protection is applied by Pythia-Webview2 itself.
Applications with stricter requirements may replace the default store with another ISecretStore implementation, for example DPAPI-backed storage, an encrypted local store, an enterprise vault, or a hardware-backed secret provider. The API key service does not need to be changed as long as the replacement keeps the same ISecretStore contract.
The WebView2 surface is intended to host Pythia-Webview2's local UI: assets/index.htm and the JavaScript templates shipped with the project. It is not intended to behave as a general-purpose browser.
The embedded UI is constrained by two layers:
- a Content Security Policy declared in
assets/index.htm; - a host-side navigation filter that blocks unauthorized external navigation.
The default CSP allows the local UI context and selected trusted origins used by the frontend dependencies, such as https://cdn.jsdelivr.net, as well as the local application origin used for media resources, such as https://app.local.
Applications requiring a fully offline deployment should package CDN-hosted dependencies locally and adjust the CSP accordingly.
Custom templates should use the existing WebView2 bridge:
window.chrome.webview.postMessage({ event: "custom-event", name: "...", payload: {} });They should not rely on arbitrary browser navigation to communicate with the host application.
Any change that relaxes the CSP, allows additional navigation targets, loads remote UI content, or renders untrusted HTML/Markdown output should be treated as a security-sensitive change.
The chat UI renders rich content, including Markdown and code blocks. Host applications must not inject user-provided HTML into the DOM without escaping or sanitizing it first.
Use WVPythia.Strings.Escape.pas for the built-in escaping helpers:
EscapeJSStringwhen injecting text into a JavaScript literal;EscapeHTMLwhen rendering text into the DOM.
Pythia-Webview2 does not currently provide a dedicated URL encoder. Add one in the host application if URLs are built from user-controlled input.
The default WebView2 permission surface is intentionally narrow.
When browser-side audio capture is enabled by the host application, microphone access may be granted automatically to support local audio features such as Speech-to-Text. This permission is limited to COREWEBVIEW2\_PERMISSION\_KIND\_MICROPHONE.
Other WebView2 permissions are not granted automatically by the default implementation.
Applications that relax the navigation policy, load remote UI content, or render untrusted HTML/Markdown output should review the permission policy before deployment.
The custom-event channel is deliberately flexible: Pythia-Webview2 forwards the raw JSON payload to the application adapter and does not impose a typed schema on it.
This means that validation is the responsibility of the application code.
Recommended practice:
- require a stable
namefield for routing; - validate the presence and shape of
payload; - reject unknown event names;
- define a
requestIdconvention if the host application needs request/response correlation; - never execute privileged native operations directly from unvalidated JavaScript payloads.
The bridge is intentionally small. Structured responses, authorization rules, and business-level validation belong to the host application.
All JavaScript templates execute in the same WebView2 context. Pythia-Webview2 does not provide a separate JavaScript sandbox for user-authored templates.
Custom templates should therefore be treated as trusted application code. They should keep their global footprint minimal, prefer IIFE-style encapsulation, and communicate with Delphi only through the documented postMessage channel.
JSON parsing should go through the framework helpers, especially:
TJsonReader.Parse(...)Do not share a TJsonReader instance across threads.
For asynchronous vendor calls, copy mutable prompt state into a stable value representation before capturing it in callbacks. In practice, vendor services should convert TInputPromptState to TStateBuffer as soon as AsyncAwaitStreamChat starts.
Pythia-Webview2 provides the UI, event bridge, persistence helpers, and integration contracts. It does not validate vendor API policies, business authorization rules, external tool execution, card payload semantics, or organization-specific compliance requirements.
Those decisions belong to the host application.
A deployment that keeps the WebView2 surface local, blocks unauthorized navigation, validates custom events, and treats CSP/permission changes as security-sensitive remains within the intended security model.
Most likely, the service adapter is missing, assigned too late, or the wrong service unit is being used.
Check the main form:
Pythia := TVCLPythia.Create(Panel2);
Pythia.ServiceAdapter := TVCLChatManagedItemDialogService.Create;
Pythia.Update;For FMX, also check AttachHost(Self):
Pythia := TFMXPythia.Create(Layout1);
Pythia.AttachHost(Self);
Pythia.ServiceAdapter := TFMXChatManagedItemDialogService.Create;
Pythia.Update;The Pythia-Webview2 UI loaded, but no service adapter was available when the communication chain initialized.
Typical causes:
ServiceAdapterwas not assigned;- it was assigned after
Update; VCL.WVPythia.Services.pasorFMX.WVPythia.Services.paswas not added to the Delphi project;- the VCL service unit was used in an FMX project, or the reverse;
- a manually written adapter did not override the required methods.
Check WebView2Loader.dll.
It must be placed next to the executable and must match the target architecture:
bin32\WebView2Loader.dll → Win32 executable
bin64\WebView2Loader.dll → Win64 executable
The target machine must also have the Microsoft Edge WebView2 Evergreen Runtime installed.
Check that the complete assets folder is present at the same level as bin32 and bin64:
MyProject
├── assets
├── bin32
└── bin64
Do not copy only index.htm. The scripts, media, and lang subfolders are part of the runtime surface.
Check three things:
- the relevant
Integration*capability is enabled; - the matching card JSON is valid;
- you are editing the active support folder, for example
bin32\MyProject\supportrather than another copied folder.
The prompt state carries selected card identities such as Id and Name. It does not necessarily carry the full content payload from the card JSON.
Resolve Item.Id inside the service layer, load the matching card definition, parse content, then inject the schema or manifest into the vendor request.
Check that the same logical key name is used everywhere:
TAnthropicServices.API_KEY_NAME = 'anthropic'The constructor, secret store read, automatic /api-key new anthropic command, OnApiKeyChanged, and UpdateApiKey should all use the same constant.
Names are normalized. Anthropic, ANTHROPIC, and anthropic refer to the same logical name after normalization.
If a custom ISecretStore is used, verify that it applies the same naming policy expected by the rest of the application.
A visible or reachable operation category has no valid default model.
Check:
- the model exists in
<ExeName>-model-list.json; - the category in
<ExeName>-model-get-replace-version.jsonhasvisible: trueonly when it is supported; - the category
modelvalue matches an existing modelid; - unsupported categories are hidden with
visible: false.
You may be editing the wrong support folder, or the generated default file may not have been replaced.
Check the active folder under the running target:
bin32\<ExeName>\support
bin64\<ExeName>\support
Also check that the model list and runtime category configuration agree on category ids.
Check the JSON shape:
eventmust be literallycustom-event;namemust be present;payloadmust be present, even if empty.
The framework forwards the raw custom payload. Application-level validation and routing belong to the service adapter or host logic.
Check that TemplateAllwaysReloading(...) was called on the provider and that the application was restarted. The original method name preserves the typo Allways.
Use the two-step pattern:
- the request event records the pending action and opens the dialog;
- only
dialog-confirmation-responseexecutes the action.
Do not execute the destructive action in the initial request handler.