SageFs is daemon-first — one server, many clients. The daemon starts bare and creates sessions on demand. Each session is an isolated worker sub-process (Erlang-style fault isolation) with its own FSI, project, and file watcher. The TUI uses SageTUI's Elm Architecture (Program<Model,Msg> with SIMD cell diff), while the Raylib GUI uses the Cell[,] grid abstraction — both share the same keybindings via KeyMap and connect to the daemon via SSE. See the architecture diagram for how clients connect.
5700+ tests: Expecto unit tests, FsCheck property-based state machine tests, Verify snapshots, binary persistence property tests.
SageFs.Core/ — Shared types, rendering abstraction, KeyMap, Theme
SageFs/ — CLI tool, daemon, SageTUI client + legacy TUI
SageFs.Gui/ — Raylib GUI client (Cell[,] grid renderer)
SageFs.Tests/ — Expecto test project
sagefs-vscode/ — VS Code extension (Fable F#→JS)
sagefs-vs/ — Visual Studio extension (C# + F#)
docs/ — GitHub Pages site
The Neovim plugin lives in a separate repo: sagefs.nvim.
Both TUI and Raylib GUI consume the same pipeline:
ElmModel → SageFsRender.render → RenderRegion list
→ Screen.draw(grid, state, regions) ← writes to Cell[,]
→ Backend.emit(grid) ← TUI: ANSI string, Raylib: draw calls
- Daemon starts bare — no project, no session
- A client creates a session with a project path
- The daemon spawns a worker sub-process, loads the project, starts watching files
- Clients send code, read diagnostics, run tests — all through the daemon
- Multiple clients can connect to the same session simultaneously
SageFs auto-rewrites use → let inside nested scopes (functions, CEs) because FSI doesn't support use in those positions. This means disposables aren't auto-disposed in the REPL — fine for experiments, be aware for long sessions.
Other FSI behaviors: redefinition shadows (doesn't error), ;; boundaries are independent transactions, no [<EntryPoint>], assembly loading is session-scoped.
Rewrite logic: SageFs.Core/FsiRewrite.fs (~25 lines). PRs welcome.