Warning
Beta — Work in Progress. The webviewer is under active development. Expect rough edges and breaking changes. If you run into issues, please report them on the issue queue.
This feature adds a browser-based FileMaker script editor as a third interaction method alongside the CLI and IDE. It runs inside a FileMaker WebViewer object pointed at a local Vite dev server, giving developers a Monaco-powered editor for composing and editing scripts in the human-readable (HR) format with live conversion to fmxmlsnippet XML.
The agent/ folder can be interacted with in three ways:
- Agentic CLI interface (e.g. Claude Code)
- IDE with integrated agentic features (e.g. Cursor, VS Code + Copilot)
- Via a WebViewer pointed at a local or remote hosted server
Multi-panel single-page application with resizable split panes and togglable panels:
┌──────────┬────────────────────────────────────┬──────────────────┐
│ │ Monaco Editor │ │
│ Library │ (HR script text) │ AI Chat │
│ Panel │ │ (Anthropic / │
│ │ syntax highlight, completions, │ OpenAI / │
│ (toggle) │ diagnostics, script/calc modes │ Claude Code) │
│ ├─────────── drag ───────────────────┤ │
│ │ XML Preview (fmxmlsnippet) │ (toggle) │
│ │ live conversion from HR (toggle) │ │
├──────────┴────────────────────────────────────┴──────────────────┤
│ Agent Output Panel (diff editor, preview, result display) │
└──────────────────────────────────────────────────────────────────┘
Toolbar: New | Validate | Clipboard | Load Script | Library | Chat | XML | Settings
StatusBar: solution name, layout, context age, validation, unresolved refs, draft restore
All panel positions and visibility states persist to both localStorage (instant) and server (debounced) via src/layout-prefs.ts.
Frontend: Preact 10 + Monaco Editor 0.52 + Tailwind CSS 4
Build: Vite 6 + TypeScript 5.7
Server: Node.js Vite dev middleware (webviewer/server/)
Entry point: webviewer/index.html → src/main.tsx → src/App.tsx
webviewer/
├── index.html # SPA entry point
├── vite.config.ts # Vite build config (registers apiMiddleware plugin)
├── tsconfig.json
├── package.json
├── .env.example # AGENT_DIR=../agent
├── server/
│ ├── api.ts # REST endpoints (Vite middleware)
│ ├── ai-proxy.ts # AI provider routing (Anthropic / OpenAI)
│ ├── claude-cli.ts # Claude Code CLI integration (subprocess)
│ ├── file-watcher.ts # Watches CONTEXT.json, pushes WS event on change
│ ├── python.ts # Python subprocess helper (spawnPython)
│ ├── settings.ts # AI provider settings (reads/writes .env.local)
│ └── ws.ts # WebSocket setup for file-watcher events
└── src/
├── App.tsx # Root component — layout, state, toolbar actions
├── main.tsx # Preact render entry
├── styles.css # Tailwind imports + custom scrollbar styling
├── autosave.ts # Dual-layer draft persistence
├── layout-prefs.ts # Panel visibility and sizing persistence
├── ai/
│ ├── chat/ # ChatPanel, MessageList components
│ ├── key-store.ts # API key management (localStorage)
│ ├── prompt/
│ │ └── system-prompt.ts # System prompt for AI chat context
│ ├── providers/ # anthropic.ts, openai.ts, claude-code.ts, registry.ts
│ ├── settings/ # AISettings UI panel
│ └── types.ts
├── api/
│ └── client.ts # Fetch wrappers for all server endpoints
├── bridge/
│ ├── detection.ts # Detect FileMaker Web Viewer runtime
│ ├── fm-bridge.ts # FileMaker.PerformScript() bridge API
│ └── callbacks.ts # Callback routing for FM→browser calls
├── context/
│ ├── store.ts # CONTEXT.json state management
│ ├── index-parser.ts # Parse pipe-delimited index files
│ └── types.ts
├── converter/
│ ├── parser.ts # Line parser: HR text → ParsedLine[]
│ ├── hr-to-xml.ts # Main HR→XML entry point
│ ├── xml-to-hr.ts # Reverse XML→HR converter
│ ├── catalog-converter.ts # Generic catalog-driven converter
│ ├── catalog-types.ts # TypeScript interfaces for catalog entries
│ ├── id-resolver.ts # Name→ID resolution via CONTEXT.json
│ ├── step-registry.ts # Plugin registry for step converters
│ ├── steps/ # Hand-coded converters per category
│ │ ├── control.ts # If, Loop, Halt, Perform Script, etc.
│ │ ├── fields.ts # Set Field, Insert Text/File/PDF, etc.
│ │ ├── navigation.ts # Go to Layout, Portal Row, Related Record
│ │ ├── records.ts # Export/Import Records, Save as PDF/Excel
│ │ ├── windows.ts # Move/Resize, Refresh, Scroll Window
│ │ └── miscellaneous.ts
├── editor/
│ ├── EditorPanel.tsx # Monaco editor wrapper
│ ├── editor.config.ts # Monaco editor configuration (font, tabs, whitespace, guides)
│ ├── language/
│ │ ├── filemaker-script.ts # Language registration
│ │ ├── monarch.ts # Syntax tokenizer (tokenizes HR script)
│ │ ├── completion.ts # Step name completions from catalog
│ │ ├── diagnostics.ts # Live validation markers
│ │ ├── fm-functions.ts # FileMaker function database for completions
│ │ ├── theme.ts # FileMaker color theme
│ │ ├── themes.ts # Theme preset manager (light/dark/Solarized)
│ │ └── theme-import.ts # Theme loading utility
│ └── xml-preview/
│ └── XmlPreview.tsx # Side-by-side fmxmlsnippet viewer
└── ui/
├── Toolbar.tsx # Top action bar
├── StatusBar.tsx # Bottom status/error display
├── LoadScriptDialog.tsx # Script search & load modal
├── LibraryPanel.tsx # Library browser with save dialog
└── AgentOutputPanel.tsx # Agent output display (diff, preview, results)
All endpoints are served by the Vite dev middleware in server/api.ts. The agentDir() function resolves to the sibling agent/ folder; mainAgentDir() follows worktree links to the main repo (see Path Resolution below).
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/context |
Returns agent/CONTEXT.json |
| GET | /api/settings |
Returns user settings (provider, model, configured providers — no raw API keys) |
| POST | /api/settings |
Updates user settings (provider, model, API keys, prompt marker) |
| POST | /api/chat |
Streams AI chat (SSE) via ai-proxy.ts |
| GET | /api/custom-instructions |
Returns agent/config/.custom-instructions.md content |
| POST | /api/custom-instructions |
Saves or deletes custom instructions file |
| GET | /api/docs |
Returns combined CODING_CONVENTIONS.md and all knowledge base docs |
| GET | /api/index/:name?solution=<sol> |
Parses and returns agent/context/<sol>/<name>.index as JSON rows (auto-detects solution when only one exists) |
| GET | /api/step-catalog |
Returns agent/catalogs/step-catalog-en.json |
| GET | /api/steps |
Lists all snippet XML files from snippet_examples/steps/ |
| GET | /api/snippet/:category/:step |
Returns XML content of a specific snippet file |
| POST | /api/validate |
Runs validate_snippet.py on posted XML; returns {valid, errors, warnings} |
| POST | /api/clipboard/write |
Writes posted XML to macOS clipboard via clipboard.py write |
| POST | /api/clipboard/read |
Reads FM objects from macOS clipboard via clipboard.py read |
| POST | /api/convert/hr-to-xml |
Stub (conversion is client-side; exists for headless use) |
| POST | /api/convert/xml-to-hr |
Stub (conversion is client-side; exists for headless use) |
| GET | /api/scripts/search?q=<query> |
Searches scripts.index by ID, exact name, or token match; returns top 20 |
| GET | /api/scripts/load?id=<id>&name=<name> |
Loads script HR (.txt) and converts SaXML to snippet via fm_xml_to_snippet.py |
| GET | /api/layout-prefs |
Returns agent/config/.layout-prefs.json (panel visibility/sizing) |
| POST | /api/layout-prefs |
Saves layout preferences |
| GET | /api/autosave |
Returns agent/config/.autosave.json |
| POST | /api/autosave |
Saves draft to agent/config/.autosave.json |
| DELETE | /api/autosave |
Deletes the autosave file |
| GET | /api/library |
Enumerates all .xml and .md files in agent/library/ by category |
| GET | /api/library/item?path=<path> |
Returns contents of a specific library item file |
| POST | /api/library/save |
Saves a library item to disk (with path traversal protection) |
| GET | /api/agent-output |
Returns agent/config/.agent-output.json (CLI agent output for webviewer) |
| DELETE | /api/agent-output |
Deletes the agent output file |
| GET | /api/sandbox |
Lists .xml files in agent/sandbox/ |
| GET | /api/sandbox/:filename |
Returns contents of a sandbox XML file |
| POST | /api/sandbox/:filename |
Writes content to a sandbox XML file |
HR script text is converted to fmxmlsnippet client-side (in the browser) so the conversion is always available without a server round-trip.
HR text
↓
parseScript() [converter/parser.ts]
— line-by-line → ParsedLine[]
↓
hrToXml() [converter/hr-to-xml.ts]
— for each line:
├─ look up converter in step-registry
├─ hand-coded? → steps/{control,fields,navigation,...}.ts
└─ fallback? → catalog-converter.ts (generic, catalog-driven)
— maps StepParam types to XML emission
— handles boolean, enum, calculation, namedCalc, field/layout/script
↓
ID resolution [converter/id-resolver.ts]
— resolveField(), resolveLayout(), resolveScript(), resolveTable()
— looks up in CONTEXT.json; falls back to id=0
— tracks failures as UnresolvedRef[] for status bar display
↓
fmxmlsnippet XML
Reverse path (xml-to-hr.ts): fmxmlsnippet → HR. Used when loading a script from the solution via the Load Script dialog.
UnresolvedRef tracking: When an ID cannot be resolved from CONTEXT.json, the converter records it. The StatusBar displays these as warnings (e.g. Unresolved layout: "Dashboard" (id will be 0)).
agent/catalogs/step-catalog-en.json is the canonical index for all FileMaker script steps. The file was bootstrapped by generate-step-catalog.ts (now archived as .old) which seeded entries from snippet_examples/ XML files — that generator is not part of the repo and should never be run again.
The catalog is maintained manually. All additions and modifications follow the process in agent/catalogs/UPDATING_CATALOGS.md. Key points from that process:
- Never read the full JSON file (it is large). Use
grep -A 60 '"name": "Step Name"' agent/catalogs/step-catalog-en.jsonto extract a single entry. - Status values:
"auto"(seeded, not reviewed) ·"complete"(reviewed with authoritative HR data) ·"unfinished"(partially reviewed) - Updates set the correct
id,hrSignature,hrLabelvalues, enum lists in HR display order, andstatus: "complete". - Shared enum reference files (
animation-enums.md,window-enums.md,language-enums.md,shared-enums.md,find-requests.md) live alongside the catalog inagent/catalogs/to avoid duplication across step entries — do not inline these into the JSON; reference them during editing only.
In the webviewer, the catalog serves three roles:
- Monaco completions (
src/editor/language/completion.ts) — step names offered as autocomplete suggestions - Live diagnostics (
src/editor/language/diagnostics.ts) — unknown step names flagged as errors - Converter registration (
catalog-converter.ts) — any step without a hand-coded handler gets a generic converter generated from its catalog entry at startup
Architecture note — why a single file: The catalog is a compiled lookup index over all steps. A single JSON file is the natural shape for a lookup table: one GET request at startup, one in-memory parse, O(1) lookup by step name. The snippet_examples/ folder is split because each file is a discrete XML artifact used individually — do not mirror that structure for the catalog. If the catalog grows significantly, the natural split would be per-category (14 files) with a ?category= filter on the API, not per-step.
The en in step-catalog-en.json is the ISO 639-1 two-character language code for English. FileMaker's Script Workspace displays step names and parameter labels in the user's application language, so each supported language requires its own catalog file.
To add a language:
- Duplicate
step-catalog-en.jsonand rename it using the appropriate ISO 639-1 code (e.g.,step-catalog-de.jsonfor German,step-catalog-fr.jsonfor French). - Open
agent/catalogs/UPDATING_CATALOGS.mdand note at the top which language file you are working on — the process is otherwise identical to the English catalog. - Work through the entries, replacing English
name,hrSignature,hrLabel, andenumValueswith their localized equivalents.
The webviewer's /api/step-catalog endpoint would need to be updated to accept a ?lang= parameter (or read from user settings) to serve the appropriate file.
Three provider options, selected in the AI Settings panel:
| Provider | Implementation | Notes |
|---|---|---|
| Anthropic | src/ai/providers/anthropic.ts |
Direct API key, streams via SSE |
| OpenAI | src/ai/providers/openai.ts |
Direct API key, streams via SSE |
| Claude Code CLI | server/claude-cli.ts + src/ai/providers/claude-code.ts |
Spawns claude subprocess; no API key needed if already authenticated |
Provider registration is managed by src/ai/providers/registry.ts, which exposes the available providers to the settings UI and chat system.
The system prompt (src/ai/prompt/system-prompt.ts) provides the AI with context about the current CONTEXT.json, the step catalog, FileMaker script conventions, and any custom instructions configured by the developer. Coding conventions and knowledge base docs are fetched from the server via /api/docs and injected into the system prompt alongside the step catalog signatures and formatted CONTEXT.json. Custom instructions (/api/custom-instructions) are appended when present, giving the developer a way to add per-session behavioral guidance without modifying the system prompt source.
The two interaction modes use fundamentally different context delivery strategies:
| Capability | CLI / IDE (Claude Code) | Webviewer AI |
|---|---|---|
| Filesystem access | Full (read/write) | None |
| CONTEXT.json | Read on demand via tool call | Formatted and injected at startup |
| Coding conventions | Read on demand | Injected into every system prompt |
| Knowledge base | Selective: scans MANIFEST, reads only matching docs | All docs injected wholesale |
| Step catalog | Grepped per-step (~60 lines each) | All known HR signatures injected |
Index files (context/*.index) |
Grepped on demand | Not available |
xml_parsed/ |
Grepped on demand | Not available |
| Script validation | Runs validate_snippet.py subprocess |
Via /api/validate endpoint |
| Clipboard | Runs clipboard.py subprocess |
Via /api/clipboard endpoints |
| Token cost | Variable — only what's needed, when needed | Fixed upfront injection on every request |
| Output format | fmxmlsnippet XML → written to agent/sandbox/ |
HR script text → converted client-side |
| Multi-step workflows | Full agentic tool use | Single-turn chat |
The CLI agent can also access the snippet_examples/ templates for complex steps and can run arbitrary shell commands as part of its toolchain. The agent/library/ of reusable snippets is available in the webviewer via the Library panel (/api/library endpoints) but is not injected into the AI system prompt.
Every AI request in the webviewer carries a fixed system prompt overhead. With all resources injected, the breakdown is:
| Resource | Approx. tokens | Notes |
|---|---|---|
| Base instructions | ~400 | Format rules, output constraints |
| Step catalog (known signatures) | ~3,800 | 197 steps with HR signatures |
| CONTEXT.json (formatted) | ~500 – 2,000 | Varies by solution size |
| Coding conventions | ~2,100 | agent/docs/CODING_CONVENTIONS.md |
| Knowledge docs (all) | ~24,000 | 14 focused docs (~98 KB total); largest are found-sets.md (~12 KB) and error-handling.md (~10 KB) |
| Custom instructions | variable | Developer-defined via /api/custom-instructions; 0 when not configured |
| Total (approximate) | ~31,000 – 33,000 | Before conversation history and custom instructions |
The knowledge base was previously dominated by a single ~45 KB terminology.md file. That glossary has since been moved to agent/docs/reference/ (not injected) and the knowledge folder split into 14 focused behavioral documents. The total injection cost is higher, but each document is targeted guidance rather than broad reference material.
The CLI approach is selective and efficient: the MANIFEST is scanned for keyword matches against the current task, and only relevant docs are read. The webviewer approach is simpler — no filesystem access means no MANIFEST-based filtering — but uses a fixed token budget on every request regardless of relevance.
Options for managing token cost as the knowledge base grows:
- Selective injection — match knowledge doc keywords against the
taskfield in CONTEXT.json and only inject relevant docs (mirrors the CLI approach) - Exclude reference docs — documents like
terminology.mdare reference material, not behavioral guidance; they are less useful pre-injected than the behavioral docs (found-sets.md,field-references.md) - User setting — expose a toggle in AI Settings to enable/disable knowledge injection per session
The webviewer's primary runtime environment is a FileMaker Web Viewer object, not a standalone browser. FileMaker embeds a WebKit-based webview that loads the Vite dev server URL. While the app functions fully in a browser (useful for development), any feature that interacts with FileMaker — context delivery, script loading, clipboard — is designed around the webviewer context.
There are two distinct paths by which CONTEXT.json can reach the client, and they behave differently:
| Path | How it works | When it fires |
|---|---|---|
| Direct JS bridge | FileMaker's Perform JavaScript in Web Viewer step calls window.pushContext(json) → setContext() in App.tsx |
When the Push Context companion script calls into the webviewer directly |
| File on disk | Push Context writes CONTEXT.json to disk; the client polls /api/context and detects the change via JSON hash comparison |
When the script runs in a separate window, or via any other process that writes the file |
Polling (path 2) is the reliable fallback. Vite's HMR WebSocket and import.meta.hot custom events are not reliable inside a FileMaker WebKit webviewer — the WebSocket connection may not deliver custom broadcast events in that environment. Any feature that needs to react to server-side file changes should use HTTP polling rather than WebSocket/HMR events.
The Load Script dialog uses polling (1.5s interval, active only while waiting) to detect context changes. The Toolbar Refresh button is a manual on-demand fetch. The window.pushContext global handles the direct bridge path.
When running inside a FileMaker WebViewer object (vs. a browser), the bridge layer enables bidirectional communication:
- Detection (
src/bridge/detection.ts): Checks forwindow.FileMakerto determine runtime context - FM → Browser: FileMaker calls a named JavaScript function via the WebViewer's
Perform JavaScript in Web Viewerstep - Browser → FM (
src/bridge/fm-bridge.ts): CallsFileMaker.PerformScript(name, param)to trigger FM scripts - Callbacks (
src/bridge/callbacks.ts): Routes incoming FileMaker calls to registered handlers
The following functions are registered on window by App.tsx at mount time, making them callable from FileMaker's Perform JavaScript in Web Viewer step:
| Function | Purpose |
|---|---|
window.pushContext(json) |
Receives CONTEXT.json directly from the Push Context companion script |
window.loadScript(content) |
Loads an existing script's HR text into the editor |
window.onClipboardReady() |
FileMaker notifies the webviewer after a clipboard write completes |
window.triggerAppAction(actionId) |
Routes toolbar actions from custom menu items (e.g. agfm.newScript, agfm.validate, agfm.clipboard, agfm.loadScript, agfm.toggleXmlPreview, agfm.toggleChat, agfm.toggleLibrary) |
window.triggerEditorAction(actionId) |
Routes Monaco editor actions (undo, redo, find, etc.) from custom menu items |
The app functions fully in a browser without FileMaker present; the bridge layer degrades gracefully.
The webviewer runs inside a FileMaker WebViewer object set to the Vite dev server URL (http://localhost:8080). The object must be named agentic-fm — this name is used by the bridge script to target the correct viewer when passing actions from custom menu items.
The web viewer can be placed on any layout, but a dedicated layout is strongly recommended:
- Place only the single web viewer object on the layout, leaving no other interactive objects
- Set the web viewer to be resizable using the autosizing anchors in the Inspector palette. This is so developers can expand it to a comfortable working size
A dedicated layout avoids interference from other layout objects and ensures the custom menu set (which is assigned per-layout) applies consistently whenever the editor is open.
This is a very beneficial, although optional, addition to using the webviewer feature of this project!
The filemaker/custom_menu/ folder contains a pre-built menu set that adds five editor-aware menus — File, Edit, Selection, Format, View — to the layout hosting the web viewer. Each menu item routes a Monaco action ID through a bridge script to the agentic-fm web viewer object.
Because FileMaker uses solution-specific UUIDs and script IDs, the files cannot be pasted directly — an agent must substitute your solution's IDs first. See filemaker/custom_menu/README.md for the step-by-step integration process.
After integrating the custom menu set, you need to set the agentic-fm custom menu as the default within the Layout Setup… dialog for the layout.
FileMaker WebViewer objects reinitialize frequently (layout changes, window switches), which wipes any in-memory state. The autosave system persists the editor content across these cycles.
Dual-layer storage:
- localStorage — written immediately on every edit (fast, synchronous)
- Server (
agent/config/.autosave.json) — written via debounced POST (2s delay), survives localStorage wipes
Restore logic on init:
- Try localStorage first
- Fall back to server GET
/api/autosave - Skip restore if content matches the default boilerplate
The StatusBar displays Restored draft: <ScriptName> when a draft is recovered.
# Start the dev server
cd webviewer
npm install # first time only
npm run dev # Vite dev server at http://localhost:8080
# In FileMaker: set the WebViewer URL to http://localhost:8080
# Or open in any browser for standalone useThe port is set to 8080 with strictPort: true in vite.config.ts. Change the port value there if a different port is needed.
Environment (copy .env.example → .env.local):
AGENT_DIR=../agent # path to agent/ relative to webviewer/
Build for production:
npm run build # outputs to webviewer/dist/If a feature for the webviewer is being worked on within a git worktree, gitignored directories (agent/context/, agent/xml_parsed/) exist only in the main repository, not in the worktree copy. The mainAgentDir() function in server/api.ts handles this transparently:
- Reads
.gitat the repo root - If
.gitis a file (worktree indicator), parses thegitdir:path - Follows
<gitdir>/../..to find the main repo root - Returns
<main-repo>/agent/
Any endpoint that reads context or xml_parsed data uses mainAgentDir(). Endpoints that write (sandbox, autosave) use the local agentDir() so worktree writes don't bleed into the main repo.
Index files are organized under agent/context/{solution}/ subfolders. The resolveContextDir() helper in server/api.ts resolves the correct subfolder — auto-detecting when only one solution exists, or using the ?solution= query parameter in multi-solution setups.
agent/catalogs/ contains the pre-compiled step catalog. The webviewer is the primary consumer; CLI agents also reference it for hrSignature lookups and parameter validation when composing scripts.
step-catalog-en.json— one entry per FileMaker script step- Originally generated from
snippet_examples/+ hardcoded step IDs and HR signatures
Monaco editor options are centralized in src/editor/editor.config.ts. Edit this file to change editor behavior without touching the component code.
// src/editor/editor.config.ts
export const editorConfig = {
fontSize: 14,
tabSize: 4,
insertSpaces: false, // false = tab characters; true = spaces
wordWrap: "on",
renderWhitespace: "selection",
// ...
};The fixed runtime options (value, language, theme, automaticLayout) remain in EditorPanel.tsx and are not part of the config file.
The agent output channel allows a CLI/IDE agent (e.g. Claude Code) to push results into the webviewer for review without the developer switching windows. This enables a workflow where the developer requests a script via the CLI, the agent writes the result, and the webviewer displays it for inspection, diffing, and clipboard deployment.
- CLI side: The companion server's
POST /webviewer/pushendpoint writes output toagent/config/.agent-output.json - Server side:
GET /api/agent-outputserves the file;DELETE /api/agent-outputclears it - Client side:
AgentOutputPanel.tsxpolls for output and displays it with a diff editor, preview pane, and result metadata
The Library panel (src/ui/LibraryPanel.tsx) provides browsable access to the agent/library/ collection of reusable fmxmlsnippet code directly within the webviewer. It supports:
- Category-organized browsing of scripts, steps, functions, fields, layouts, menus, and webviews
- Reading library item contents for reference or adaptation
- Saving new or modified items back to the library via
/api/library/save
The panel is togglable via the toolbar and its width is persisted in layout preferences.
- Index file fallback in id-resolver: Unresolved layout/field/script names that are absent from CONTEXT.json currently emit
id=0. Adding a fallback toagent/context/*.indexfiles would resolve names from the full solution. - Go to Related Record converter: The hand-coded converter for this step is not yet implemented; falls back to catalog-driven (partial support).
- Set Variable repetition syntax:
$name[rep]repetition notation is passed through as-is without validation. - Server-side conversion: The
/api/convert/hr-to-xmland/api/convert/xml-to-hrendpoints are stubs. A server-side converter would enable headless script conversion (CI pipelines, CLI calls without a browser). - Agent output channel wiring: The
/webviewer/pushendpoint andAgentOutputPanelare built, but skills have not yet been wired to push output automatically at the end of a build.