Documents build commands, architecture, WKWebView constraints, conventions, the protocol addition checklist, and database migration patterns.
6.8 KiB
Copilot Instructions — Nexus Terminal
Build & Test
make deps # go mod download + npm install
make build # production build → build/bin/nexus-terminal.app
make dev # wails dev with hot reload
make test # go test ./... -v -count=1
make lint # golangci-lint run ./...
# Single package test
go test ./internal/storage/ -v -count=1 -run TestProfileCRUD
The frontend has no separate build step — npm run build (called by Wails) copies xterm vendor libs from node_modules into frontend/src/js/lib/ and frontend/src/css/lib/. All frontend assets are embedded at compile time via frontend/embed.go.
Architecture
Wails v2 desktop app: Go backend + HTML/CSS/JS frontend rendered in a native webview (WKWebView on macOS).
Go backend (internal/):
app/— lifecycle bootstrap: opens SQLite, inits secret stores, wires everything togetherapi/— singleServicestruct bound to Wails; every public method is callable from JS aswindow.go.api.Service.MethodName()sessions/— manages concurrent protocol sessions; owns SSH/Telnet/RAW/GitHub client lifecyclesstorage/— SQLite repository (WAL mode) with embedded sequential migrations instorage/migrations/secrets/— two-tier credential storage: OS keychain (preferred) → AES-256-GCM encrypted file vault (fallback)models/— shared domain types and defaults; protocol configs are JSON-encoded in the DBsshclient/,telnetclient/,rawclient/,githubclient/— protocol implementations
Frontend (frontend/):
- Plain HTML/CSS/JS — no framework, no bundler, no ES modules
index.htmlis the app shell; loads CSS then JS in strict ordersrc/js/— 13 modules, each IIFE-wrapped, exporting towindow.NexusTerminalsrc/css/— modular stylesheets;tokens.cssdefines all design tokens- Vendor libs (xterm.js, addons) live in
src/js/lib/andsrc/css/lib/— auto-copied, gitignored
Go ↔ JS communication:
- Synchronous: Frontend calls
window.go.api.Service.*()(returns Promise) - Asynchronous: Backend pushes events via
wailsRuntime.EventsEmit(); frontend listens withwindow.runtime.EventsOn() - Bidirectional events for secret prompts and host key verification: backend emits a request event with a
requestID, frontend shows a modal, then emits a response event with the same ID. The Go side blocks on a channel keyed by that ID.
Key events: session:data, session:state, session:closed, session:raw:data, session:github:repos, session:secret:prompt/session:secret:response, session:hostkey:verify/session:hostkey:response.
WKWebView Constraints (Critical)
The macOS webview silently blocks or breaks several standard web APIs. These constraints affect every frontend change:
-
No
window.confirm(),window.prompt(),window.alert()— they either return false/null silently or do nothing. All user prompts must use the custom modal system (DOM-based backdrop + modal withis-visibleclass toggling). -
No ES modules —
<script type="module">fails silently. All JS files use plain<script>tags and IIFE wrappers:(function() { ... })(); -
Click swallowing on text-selectable elements — WKWebView initiates a text selection gesture on
mousedownfor<div>elements, which swallows the firstclickevent (appears to require double-click). Fix: addmousedownhandler withe.preventDefault()on any clickable non-input element (section headers, list items, etc.). -
Modal visibility pattern — both
.modal-backdropAND.modalelements start withopacity: 0; visibility: hiddenin CSS. You must add theis-visibleclass to both elements, not just the backdrop. -
Wails runtime injection —
window.goandwindow.runtimeare injected by Wails before scripts execute. Do NOT add explicit script tags for Wails scripts. The frontend polls for their availability at startup (max 5s).
Conventions
Language: All code comments in British English (colour, initialise, behaviour, etc.).
Go patterns:
- String-typed enums:
type Protocol stringwithconst ProtocolSSH Protocol = "ssh"etc. - Protocol configs are pointer fields on
Profile(SSH *SSHConfig,Telnet *TelnetConfig, etc.) — nil when not applicable NewProfile(protocol)factory indefaults.gopopulates sensible defaults per protocol- Storage uses
nullableJSON()helper to write nil for unused config columns - Tests use
t.TempDir()for isolation,t.Fatalf()for setup failures
Frontend patterns:
- Every JS file is IIFE-wrapped and exports to
window.NexusTerminalnamespace - Script load order matters (defined in
index.html) — utils → state → notifications → sidebar → tabs → terminal → profiles → raw → github → settings → commands → secrets → app - DOM helpers:
createElement(tag, className, attrs)from utils.js (supportstextContent,innerHTML,dataset,on*event handlers) - Profile editor uses
_renderSection(title, fields)pattern with_field(type, name, label, value, opts)helper for form generation protocolColour()returns CSS class names likeproto-ssh; tab badges usebadge-sshclass naming (different pattern)
CSS patterns:
- Design tokens in
tokens.css:--bg-base,--bg-surface,--bg-elevated,--text-primary,--accent,--protocol-ssh, etc. - Dark charcoal palette — not Material Design literally, but Material-inspired layering
- Protocol colours: SSH=teal
#4ecdc4, Telnet=amber#ffa726, RAW=purple#ab47bc, GitHub=indigo#7c8aff
Adding a new protocol:
- Add
Protocolconstant + config struct inmodels/models.go - Add defaults in
models/defaults.goNewProfile()switch - Create client package in
internal/<proto>client/ - Add
connect<Proto>()insessions/manager.go+ case inConnect()switch - Add API methods in
api/api.goif needed - Add DB migration in
storage/migrations/(update CHECK constraint, add config column) - Update
storage/store.go: all SELECT/INSERT/UPDATE queries + both scan functions - Frontend: add DEFAULTS in
profiles.js, render sections, collect form values, protocol tab - Frontend: add viewer in new JS file (IIFE-wrapped), include in
index.html, wire inapp.js - Add CSS: protocol colour token, sidebar indicator, tab badge, viewer styles
Database migrations:
- Numbered SQL files:
001_initial.sql,002_github_protocol.sql, etc. - Each must
INSERT INTO schema_version (version, applied_at) VALUES (N, datetime('now'))at the end - SQLite has no
ALTER COLUMN— use the recreate-table pattern (CREATE new → INSERT INTO new SELECT FROM old → DROP old → ALTER TABLE RENAME) - Every query in
store.gothat touches the profiles table must be updated when columns change (there are ~8 SELECT column lists + 2 scan functions)