Replace hand-rolled LSP client (lspClient.ts, useLspClient.ts) with monaco-languageclient v10 extended mode using @typefox/monaco-editor-react. NWScript TextMate grammar from the LSP submodule provides syntax highlighting. Full LSP features: completion, hover, diagnostics, go-to-definition, signature help — all wired through WebSocket to the nwscript-language-server. LSP server patches: fix workspaceFolders null assertion crash, handle missing workspace/configuration gracefully, derive rootPath from rootUri when null, guard tokenizer getRawTokenContent against undefined tokens. Backend fixes: WebSocket routing changed to noServer mode so /ws, /ws/lsp, and /ws/terminal/* don't conflict. TLK index loaded at startup (41,927 entries from nwn-haks/layonara.tlk.json). Workspace routes get proper try/catch. writeConfig creates parent directories. setupClone ensures workspace structure. Frontend: GffEditor and AreaEditor rewritten with inline styles and TLK resolution for CExoLocString fields. EditorTabs rewritten with lucide icons. Tab content hydrates from API on refresh. Setup wizard gets friendly error messages. SimpleEditor/SimpleDiffEditor for non-LSP editor uses. Vite config updated for monaco-vscode-api compatibility.
26 KiB
Layonara Forge — Agent Handoff Document
What Is This
Layonara Forge is a purpose-built NWN (Neverwinter Nights) Development IDE that runs as a local web application in Docker. It lets contributors fork, clone, edit, build, and run a complete Layonara NWNX NWN server with zero native tooling — only Docker required. The project name was chosen during brainstorming: "Forge" evokes crafting/building in a fantasy context.
Project Location
- Forge codebase:
/home/jmg/dev/layonara/layonara-forge/ - Design spec:
/home/jmg/dev/docs/superpowers/specs/2026-04-20-layonara-forge-design.md - Implementation plans:
/home/jmg/dev/docs/superpowers/plans/2026-04-20-forge-plan-{1..6}-*.md - Gitea integration plan: See Cursor plan file
gitea_+_forge_integration_8e0df077.plan.md - Design context:
.impeccable.mdat project root (personality: arcane, precise, deep)
Architecture
Host Machine
├── Browser → localhost:3000 (Forge UI)
├── NWN Toolset → writes GFFs to modules/temp0/
├── ~/layonara-workspace/ (repos, server data, config)
└── Docker Socket
Docker Network
├── layonara-forge (Node.js + React, serves UI, manages everything via Docker socket)
├── layonara-builder (ephemeral: nasher, nwn_script_comp, layonara_nwn, cmake, gcc)
├── layonara-nwserver (ghcr.io/plenarius/unified — NWN:EE + NWNX)
└── layonara-mariadb (mariadb:10.11 — game database)
The Forge container manages sibling containers via the Docker socket (Portainer pattern). Contributors clone one repo, set two paths in .env, run docker compose up, and open a browser.
Tech Stack
- Backend: Node.js 20 + Express 5 + TypeScript (ES modules)
- Frontend: React 19 + Vite 6 + Tailwind CSS 4 (utility classes unreliable — inline styles used for layout)
- Icons: lucide-react (SVG icons throughout)
- Fonts: Self-hosted variable fonts via @fontsource-variable (Manrope, Alegreya, JetBrains Mono)
- Code editor: Monaco Editor with NWScript Monarch tokenizer
- NWScript LSP: Forked
layonara/nwscript-ee-language-serverconnected viamonaco-languageclientWebSocket. Server patched for Forge compatibility (see LSP server patches below). - Terminal: xterm.js with child_process.spawn shell sessions
- Docker API: dockerode
- Git: simple-git (named import:
import { simpleGit } from "simple-git") - Git provider: Gitea at
https://gitea.layonara.com(NOT GitHub — see Gitea section) - Real-time: WebSocket via ws library
What's Built (55 commits + UI overhaul session)
All 6 implementation plans are complete. The codebase compiles and both Docker images build successfully. A complete UI/UX overhaul was performed (see "UI/UX Overhaul" section below).
Backend Services (packages/backend/src/services/)
| Service | File | Purpose |
|---|---|---|
| WebSocket | ws.service.ts |
Event broadcasting to all connected clients |
| Workspace | workspace.service.ts |
Directory structure management, forge.json config |
| Docker | docker.service.ts |
Container CRUD, image pulls, ephemeral container runs |
| Build | build.service.ts |
Module compile/pack, hot-reload, hak builds, NWNX builds |
| Server | server.service.ts |
NWN server stack lifecycle (start/stop/restart MariaDB + nwserver) |
| Toolset | toolset.service.ts |
temp0/ file watcher, GFF→JSON conversion, change management |
| Editor | editor.service.ts |
File CRUD, directory trees, workspace search |
| Git | git.service.ts |
Clone, pull, commit, push, diff, upstream polling |
| Git Provider | git-provider.service.ts |
Gitea API (fork, PR, token validation) |
| Terminal | terminal.service.ts |
Shell session management via child_process |
| LSP | lsp.service.ts |
NWScript language server process management |
Backend Routes (packages/backend/src/routes/)
| Route | Prefix | Endpoints |
|---|---|---|
| workspace | /api/workspace |
GET /config, PUT /config, POST /init |
| docker | /api/docker |
containers, pull, start/stop/restart, logs |
| build | /api/build |
module/compile, module/pack, deploy, compile-single, haks, nwnx |
| server | /api/server |
status, start, stop, restart, generate-config, seed-db, sql |
| toolset | /api/toolset |
status, start, stop, changes, apply, apply-all, discard |
| editor | /api/editor |
tree, file CRUD, search, resref, tlk, 2da, gff-schema |
| github | /api/github |
validate-pat (actually Gitea token), fork, forks, pr, prs |
| repos | /api/repos |
clone, list (/), status (/:repo/status), pull, commit, push, diff |
| terminal | /api/terminal |
sessions CRUD |
Frontend Pages (packages/frontend/src/pages/)
| Page | Route | Purpose |
|---|---|---|
| Dashboard | / |
Server status, repo summary, quick actions (3-column cards) |
| Editor | /editor |
Monaco editor with file explorer, tabs, GFF visual editors |
| Build | /build |
Module/hak/NWNX build sections with streaming output |
| Server | /server |
Controls, log viewer with filter, SQL console |
| Toolset | /toolset |
temp0/ watcher status, change table, diff viewer |
| Repos | /repos |
Git status cards, commit dialog, PR creation |
| Settings | /settings |
PAT, theme, editable paths, Docker images, shortcuts, reset |
| Setup | /setup |
4-phase onboarding wizard with 10 steps |
Special Features
- NWScript syntax highlighting: Monarch tokenizer with keyword/type/comment/string/preprocessor rules
- SQL highlighting in NWScript strings: Detects
NWNX_SQL_PrepareQuery()calls, highlights SQL keywords in teal - Resref auto-lookup: Backend indexes all GFF JSON files, hover on resref strings shows the item/creature/area
- TLK preview: Hover on integer literals shows the TLK string (handles 16777216 custom offset)
- 2DA intellisense: Parses 2da files, provides completion for
Get2DAStringcalls - Visual GFF editors: Form-based editors for .uti, .utc, .are, .dlg, .utp, .utm JSON files
- Conventional commit enforcement: Type dropdown (feat/fix/refactor/etc), rejects malformed messages
- Dark/light theme: OKLCH CSS custom properties toggled via
lightclass on root element
UI/UX Overhaul (April 21, 2026 session)
A complete design overhaul was performed using the Impeccable design skill system. The design context is documented in .impeccable.md.
Design System
Personality: Arcane, precise, deep — a craftsman's workbench.
Fonts (all self-hosted via @fontsource-variable, no Google Fonts):
- Body/UI: Manrope Variable — warm geometric sans
- Headings: Alegreya Variable — calligraphic serif with manuscript roots
- Code/mono: JetBrains Mono Variable
Color palette (full OKLCH, 30+ tokens in globals.css):
- Surfaces tinted toward amber (hue 65) — "warm darks, not cold ones"
- 3-level depth:
--forge-bg→--forge-surface→--forge-surface-raised - Accent: evolved gold
oklch(58% 0.155 65)with hover and subtle variants - Semantic colors: success (forest green, hue 150), danger (brick red, hue 25), warning (golden, hue 80), info (steel blue, hue 230)
- Each semantic color has base, bg, and border variants for both dark and light modes
- Log panels: dedicated
--forge-log-bg/--forge-log-texttokens
Icons: lucide-react SVG icons throughout (Code2, Wrench, Hammer, Play, GitBranch, Settings, Sun/Moon, Terminal, etc.)
Type scale (fixed rem for IDE density):
--text-xs(11px) through--text-2xl(28px), ~1.25 ratio
What Changed
Foundation:
- Replaced Inter font with Manrope Variable, Baskerville with Alegreya Variable
- Removed Google Fonts
<link>— all fonts bundled as npm deps - Full OKLCH palette replacing all hex values (~60 hard-coded colors replaced)
- All Tailwind semantic color classes (
green-400,red-500/20, etc.) replaced with forge tokens - Global CSS: scrollbar theming, selection color, input/button base styles,
:focus-visiblering,prefers-reduced-motion
IDE Shell (IDELayout.tsx):
- Lucide SVG icons replacing Unicode emoji in nav rail
- Removed 3px left border stripe (impeccable anti-pattern ban)
- Sidebar only shows on
/editorroute (was showing on all pages) - All layout uses inline styles (Tailwind flex classes were not reliably applying)
- Terminal toggle bar with Terminal/Chevron icons
All 8 pages rewritten with consistent patterns:
- Card containers:
--forge-surfacebg,--forge-border,0.75remradius - Section headers: uppercase,
--text-xs, icon + label - Buttons: accent primary, outline secondary, danger for destructive
- Status badges: semantic colors with dots
- All inline styles (Tailwind utility classes unreliable for layout in this project)
Setup wizard:
- 4-phase indicator (Environment → Authentication → Repositories → Finalize) with numbered circles + connecting lines, matching James's work app wizard pattern
- Steps reordered: Workspace + NWN Home before Gitea Token
- PathInput component with folder icon for path fields
- StatusDot component replacing emoji (✅❌⏳) with styled HTML elements
- Navigation: ghost "← Back" left, accent "Next →" right, border-top separator
Performance:
- Routes code-split via
React.lazy()— 10 chunks instead of 1 (760KB → initial 15KB app shell) - Page chunks: Editor 98KB, Setup 16KB, Repos 13KB, others 5-8KB each
Accessibility:
:focus-visibleoutline on all interactive elementsaria-label="Main navigation"on nav- Tab close button changed from
<span>to<button aria-label="Close tab"> - Toast container has
aria-live="polite"+role="status" window.confirm()guards on destructive actions (Discard All, Reset Setup)- SetupGuard shows "Loading Forge…" instead of blank screen
Important: Tailwind CSS 4 Quirk
Tailwind CSS 4 utility classes for layout (flex, flex-1, items-center, etc.) do NOT reliably apply in this project. All critical layout uses inline styles instead. This is a conscious decision, not laziness. The Tailwind @import "tailwindcss" is still loaded and works for some utilities (rounded, overflow-hidden, etc.) but do not rely on Tailwind classes for flex/grid layout. Use inline style={{}} props.
Docker Images
layonara-forge (563MB)
- Base:
node:20-slim - Multi-stage build: builder stage compiles TS + Vite, production stage has only runtime deps
- Serves React frontend as static files from Express
- The Dockerfile is at repo root:
Dockerfile
layonara-builder (577MB)
- Base:
ubuntu:24.04 - All tools installed from pre-built GitHub Release binaries (no Nim compilation)
- The Dockerfile is at
builder/Dockerfile - Tools: nwn_gff, nwn_script_comp, nasher, layonara_nwn, cmake, gcc, git
Critical: The builder Dockerfile downloads pre-built binaries from:
layonara/neverwinter.nimreleases (nwn_gff, nwn_script_comp, etc.)squattingmonk/nasher.nimreleases (nasher)plenarius/layonara_nwnreleases (layonara_nwn)
If any of these release URLs break, the builder image won't build.
Gitea Infrastructure
GitHub is no longer the primary git provider for contributors. Gitea is self-hosted on xandrial.
Setup
- Gitea URL:
https://gitea.layonara.com - Host: xandrial (159.69.30.129, Hetzner CPX41)
- Managed by: Coolify on leanthar, service UUID
xo2yy8rml79lkzmf92cgeory - Database: PostgreSQL 16 sidecar in same Coolify service
- Auth: Authentik OIDC SSO (same login as Nextcloud and email)
- SSH: Port 2222 for git-over-SSH
- Admin account:
orth
Repos on Gitea
| Repo | Branch | Push Mirror → GitHub |
|---|---|---|
layonara/nwn-module |
ee |
Yes, sync on commit |
layonara/nwn-haks |
64bit |
Yes, sync on commit |
NOT on Gitea
plenarius/unified(NWNX) stays on GitHub — read-only, no contributions through Forge
Branch Protection
eeon nwn-module: onlyorthcan push directly64biton nwn-haks: onlyorthcan push directly- Contributors must fork within the layonara org and PR
Push Mirrors
Each Gitea repo has a push mirror configured to sync to the corresponding GitHub repo. This triggers existing GitHub CI/CD and Discord bot webhooks. Commit attribution is preserved (it's in the git objects).
How the Forge Connects
GIT_PROVIDER_URLenv var (default:https://gitea.layonara.com)- Backend uses
git-provider.service.tswhich calls Gitea API at$GIT_PROVIDER_URL/api/v1 - Clone URLs:
https://<token>@gitea.layonara.com/<user>/<repo>.git - The
unifiedrepo clones from GitHub directly (no token needed, public repo)
Forked Dependencies
layonara/neverwinter.nim (fork of niv/neverwinter.nim)
- Purpose: Adds
-n(no entry point) and-E(all errors) flags tonwn_script_comp - Cherry-picked: PRs #152 and #153 from cgtudor's branches
- Release workflow:
.github/workflows/release.yml— builds on tag push, creates GitHub Release with pre-built linux tarball - Current release:
v2.1.2-layonara - Upstream status: Waiting for niv to merge the PRs. When merged, this fork can be retired.
- Remote setup:
origin= layonara fork,upstream= niv/neverwinter.nim (push disabled:no_push)
layonara/nwscript-ee-language-server (fork of PhilippeChab/nwscript-ee-language-server)
- Purpose: Migrates diagnostics from
nwnsctonwn_script_comp - Merged: PR #77 (cgtudor's tdn-hack branch) with conflict resolution
- Status: Waiting for upstream compiler PRs to merge, then PhilippeChab can merge PR #77, then this fork can be retired.
- Included in Forge: As a git submodule at
lsp/nwscript-language-server/ - Remote setup:
origin= layonara fork,upstream= PhilippeChab (push disabled:no_push)
plenarius/layonara_nwn
- Not a fork: James's own repo
- Release workflow added:
.github/workflows/release.yml— builds on tag push - Current release:
v0.1.1
Known Issues & Incomplete Work
TypeScript
- All backend TS errors are resolved (Express 5
*pathreturnsstring[], simple-git uses named import) - Frontend uses
"moduleResolution": "bundler"and"noImplicitAny": falsein tsconfig to avoid strict-mode issues from subagent-generated code - Frontend build script is
"build": "vite build"(skipstsc -bwhich fights with bundler resolution)
Tested End-to-End
- Setup wizard flow against live Gitea — fork detection, token validation, clone all 3 repos (including 5.5GB nwn-haks), workspace initialization
- NWScript LSP with real
.nssfiles — completion, hover, syntax highlighting all working viamonaco-languageclient - TLK resolution in GFF visual editors — 41,927 entries from
layonara.tlk.json
Not Yet Tested End-to-End
- Module build → server start → connect with NWN client
- Toolset temp0/ watcher with real GFF files
- Hot-reload pipeline with a real nasher build
- Push mirrors actually triggering GitHub CI
Remaining UI Work
- GFF visual editors (ItemEditor, CreatureEditor, AreaEditor, DialogEditor) still use Tailwind classes — may need inline style conversion
- CommitDialog component styling could be improved
- FileExplorer sidebar styling uses older patterns (Tailwind classes for layout)
- EditorTabs uses older patterns
- SearchPanel uses older patterns
- ErrorBoundary component not styled
- Light mode needs visual verification — all tokens have light variants but the overall look hasn't been tested
- The vendor bundle is still 598KB (Monaco dominates) — could be split further with
manualChunks
Database
db/schema.sqlcontains the full schema from James's local dev DB (nwn_dev) + seed data for cnr_misc and pwdata- DM row insertion happens during setup wizard (contributor provides CD key)
- Architecture is ready for richer seed data (production dump) but not implemented yet
Infrastructure
- Gitea on xandrial needs monitoring/backup strategy (no backup service configured yet)
- The Gitea PostgreSQL database should be backed up regularly
Key Conventions
- NEVER close GitHub issues without explicit permission — state changes fire Discord webhooks to the player community
- NEVER push to
nwnxee/unified— James's fork isplenarius/unified - No pushing layonara-forge to any remote until James says so — everything is local
- Conventional commits:
feat:,fix:,refactor:,docs:,chore: - NWN resref limit: 16 characters max for all filenames
- Express 5: Uses
*pathfor catch-all/wildcard routes, NOT bare* - simple-git: Use named import
import { simpleGit } from "simple-git", NOT default import - Inline styles for layout: Do NOT use Tailwind classes for flex/grid layout — they don't reliably apply. Use inline
style={{}}props. - CSS variables for all colors: Never use hex values. Use
var(--forge-*)tokens fromglobals.css. - lucide-react for icons: Never use Unicode emoji for UI icons. Import from
lucide-react.
Environment Variables
Forge Container
| Var | Default | Purpose |
|---|---|---|
WORKSPACE_PATH |
/workspace |
Where repos, server data, config live |
NWN_HOME_PATH |
/nwn-home |
NWN documents directory (for Toolset temp0/) |
GIT_PROVIDER_URL |
https://gitea.layonara.com |
Gitea instance URL |
PORT |
3000 |
HTTP server port |
Coolify
| Var | Location | Purpose |
|---|---|---|
COOLIFY_API_TOKEN |
~/.env.coolify |
API access to Coolify on leanthar |
COOLIFY_URL |
~/.env.coolify |
https://leanthar.layonara.com |
Gitea
| Item | Value |
|---|---|
| API token | eb79a92cea7dad657a0c81ddd2290a1be95057e2 (orth's token, name: forge-setup) |
| Gitea service UUID in Coolify | xo2yy8rml79lkzmf92cgeory |
File Structure
layonara-forge/
├── Dockerfile # Forge image (multi-stage Node.js build)
├── docker-compose.yml # Forge container with socket + workspace mounts
├── .env.example # WORKSPACE_PATH, NWN_HOME_PATH, GIT_PROVIDER_URL
├── .impeccable.md # Design context (personality, palette, principles)
├── .agents/skills/ # Impeccable design skills (18 commands)
├── builder/
│ └── Dockerfile # Builder image (pre-built binaries, no Nim)
├── db/
│ └── schema.sql # MariaDB schema + seed data
├── lsp/
│ └── nwscript-language-server/ # Git submodule (forked LSP)
├── packages/
│ ├── backend/
│ │ └── src/
│ │ ├── index.ts # Express + WS + upgrade handlers
│ │ ├── config/ # repos.ts, env-template.ts
│ │ ├── gff/ # GFF schema definitions (6 types)
│ │ ├── nwscript/ # resref-index, tlk-index, twoda-index
│ │ ├── routes/ # All API routes
│ │ └── services/ # All backend services
│ └── frontend/
│ └── src/
│ ├── App.tsx # Router with SetupGuard + React.lazy routes
│ ├── components/ # editor/, gff/, terminal/, Toast, etc.
│ ├── hooks/ # useWebSocket, useEditorState, useTheme, useLspClient
│ ├── layouts/ # IDELayout (inline styles), SetupLayout
│ ├── lib/ # lspClient.ts (JSON-RPC bridge)
│ ├── pages/ # All page components (inline styles, lucide icons)
│ ├── services/ # api.ts
│ └── styles/ # globals.css (OKLCH tokens, font imports, global styles)
Running Locally
Native (dev mode, fastest iteration)
cd /home/jmg/dev/layonara/layonara-forge
WORKSPACE_PATH=/tmp/forge-test NWN_HOME_PATH=/home/jmg/dev/nwn/local-server/home GIT_PROVIDER_URL=https://gitea.layonara.com npm run dev
# Frontend: http://localhost:5173 (proxies to backend)
# Backend: http://localhost:3000
Important: If you run without WORKSPACE_PATH, it defaults to /workspace which doesn't exist natively. The File Explorer will show "Repository not cloned" and repos will show as uncloned.
Docker (production mode)
cd /home/jmg/dev/layonara/layonara-forge
cp .env.example .env # edit paths
docker compose up -d
# Open http://localhost:3000
Building Docker images
docker build -t layonara-builder builder/ # ~45 seconds
docker build -t layonara-forge . # ~2 minutes
Current State (as of LSP integration session)
- All code is local — nothing has been pushed to any remote for the layonara-forge repo itself
- Setup wizard integration-tested against live Gitea (fork, clone all 3 repos verified)
- monaco-languageclient v10 extended mode fully integrated — replaced hand-rolled LSP client
- NWScript TextMate grammar from submodule providing syntax highlighting (replaces Monarch tokenizer)
- NWScript LSP working end-to-end: completion, hover, diagnostics, go-to-definition, signature help
- LSP server patched: 4 crash bugs fixed (workspaceFolders null, workspace/configuration, rootPath null, tokenizer null guard)
- TLK index loaded at startup (41,927 entries), resolves localized string references in GFF editors
- GFF editors rewritten with inline styles, TLK lookup for CExoLocString fields
- EditorTabs rewritten with inline styles + lucide icons
- Tab content hydrates from API on refresh (localStorage persists tab list, API fetches content)
- Setup wizard error display improved with friendly error mapping
- Backend: WebSocket routing fixed (noServer mode), workspace routes have try/catch, writeConfig creates directories
- Docker:
gitadded to production Dockerfile
Editor Stack
| Component | Package | Purpose |
|---|---|---|
| Editor wrapper | @typefox/monaco-editor-react |
React component wrapping MonacoEditorReactComp |
| LSP client | monaco-languageclient v10.7.0 |
Extended mode with VS Code services |
| VS Code API | @codingame/monaco-vscode-api v25.1.2 |
TextMate, themes, editor services |
| Grammar | nwscript-extension/ |
Local VS Code extension with .tmLanguage.json from submodule |
| Theme | Default Dark Modern | VS Code built-in (Forge Dark theme created but token colors need tuning) |
| LSP server | lsp/nwscript-language-server/server/out/server.js |
Forked NWScript LSP, --stdio mode |
| LSP bridge | packages/backend/src/services/lsp.service.ts |
WebSocket ↔ stdio pipe |
| Compiler | lsp/.../server/resources/compiler/linux/nwn_script_comp |
Pre-built binary for diagnostics |
Key Architecture Notes
monaco-editoris overridden to@codingame/monaco-vscode-editor-apivia npm overrides in rootpackage.json- The
MonacoVscodeApiWrappermust be initialized once per app lifecycle —MonacoEditorReactComphandles this SimpleEditorandSimpleDiffEditor(for Server SQL console and Toolset diff viewer) also useMonacoEditorReactCompwithout a language client- NWScript extension registered via
@codingame/monaco-vscode-api/extensionsregisterExtensionpattern - Vite config requires
resolve.dedupe: ['vscode'],worker.format: 'es', and@codingame/esbuild-import-meta-url-plugin - WebSocket routing:
/ws(event broadcast),/ws/lsp(LSP bridge),/ws/terminal/:id(terminal) — all vianoServermode with manual upgrade handling
Priority for Next Session
- Forge Dark theme — created at
nwscript-extension/themes/forge-dark.jsonbut token colors don't apply (theme loads but syntax highlighting loses color). Needs investigation into how VS Code theme contributions interact with TextMate scopes in the@codingamestack. Consider starting from Default Dark Modern and customizing editor chrome colors viauserConfigurationinstead. - Polish remaining components — CommitDialog, FileExplorer, SearchPanel still use Tailwind classes for layout. ItemEditor, CreatureEditor, DialogEditor GFF editors also need inline style conversion.
- Test module build → server start → NWN client connection — requires Docker socket access
- Test Toolset temp0/ sync with real GFF files
- Test light mode visually
- Set up Gitea backup on xandrial
- Button nesting warning — EditorTabs has
<button>inside<button>(close button inside tab button). Change outer to<div>with click handler.