Files
layonara-forge/HANDOFF.md
T
plenarius f39f1d818b feat: integrate monaco-languageclient v10 with NWScript LSP
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.
2026-04-21 05:23:52 -04:00

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.md at 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-server connected via monaco-languageclient WebSocket. 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 Get2DAString calls
  • 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 light class 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-text tokens

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-visible ring, 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 /editor route (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-surface bg, --forge-border, 0.75rem radius
  • 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-visible outline on all interactive elements
  • aria-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.nim releases (nwn_gff, nwn_script_comp, etc.)
  • squattingmonk/nasher.nim releases (nasher)
  • plenarius/layonara_nwn releases (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

  • ee on nwn-module: only orth can push directly
  • 64bit on nwn-haks: only orth can 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_URL env var (default: https://gitea.layonara.com)
  • Backend uses git-provider.service.ts which calls Gitea API at $GIT_PROVIDER_URL/api/v1
  • Clone URLs: https://<token>@gitea.layonara.com/<user>/<repo>.git
  • The unified repo 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 to nwn_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 nwnsc to nwn_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 *path returns string[], simple-git uses named import)
  • Frontend uses "moduleResolution": "bundler" and "noImplicitAny": false in tsconfig to avoid strict-mode issues from subagent-generated code
  • Frontend build script is "build": "vite build" (skips tsc -b which 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 .nss files — completion, hover, syntax highlighting all working via monaco-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.sql contains 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 is plenarius/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 *path for 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 from globals.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: git added 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-editor is overridden to @codingame/monaco-vscode-editor-api via npm overrides in root package.json
  • The MonacoVscodeApiWrapper must be initialized once per app lifecycle — MonacoEditorReactComp handles this
  • SimpleEditor and SimpleDiffEditor (for Server SQL console and Toolset diff viewer) also use MonacoEditorReactComp without a language client
  • NWScript extension registered via @codingame/monaco-vscode-api/extensions registerExtension pattern
  • 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 via noServer mode with manual upgrade handling

Priority for Next Session

  1. Forge Dark theme — created at nwscript-extension/themes/forge-dark.json but 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 @codingame stack. Consider starting from Default Dark Modern and customizing editor chrome colors via userConfiguration instead.
  2. Polish remaining components — CommitDialog, FileExplorer, SearchPanel still use Tailwind classes for layout. ItemEditor, CreatureEditor, DialogEditor GFF editors also need inline style conversion.
  3. Test module build → server start → NWN client connection — requires Docker socket access
  4. Test Toolset temp0/ sync with real GFF files
  5. Test light mode visually
  6. Set up Gitea backup on xandrial
  7. Button nesting warning — EditorTabs has <button> inside <button> (close button inside tab button). Change outer to <div> with click handler.