Overview
moha is a C++26 terminal AI client. It compiles to a single static binary (~9 MB) with no runtime dependencies beyond libc.
The architecture follows the Elm pattern: a pure-functional update loop with strong types, delegating all rendering to the maya TUI engine.
┌─────────────────────────────────────────┐
│ maya TUI Engine │ Rendering, layout, input
├─────────────────────────────────────────┤
│ View Layer │ Model → Element (pure)
├─────────────────────────────────────────┤
│ Reducer (update) │ (Model, Msg) → (Model, Cmd)
├─────────────────────────────────────────┤
│ Domain / Session │ Threads, messages, phases
├─────────────────────────────────────────┤
│ Tools + Permissions │ EffectSet, Policy, Sandbox
├─────────────────────────────────────────┤
│ Provider (API Layer) │ Claude API, streaming, OAuth
└─────────────────────────────────────────┘
Elm Architecture
The entire application is a single (Model, Msg) → (Model, Cmd) loop:
struct MohaApp {
static Model init();
static auto update(Model m, Msg msg) -> std::pair<Model, maya::Cmd<Msg>>;
static maya::Element view(const Model& m);
static auto subscribe(const Model& m) -> maya::Sub<Msg>;
};
static_assert(maya::Program<MohaApp>);
Model— the complete application state: threads, messages, tool calls, streaming state, UI state, auth credentials.Msg— a closedstd::variantof every event that can happen: key press, API delta, tool completion, timer tick, etc.update— a singlestd::visitover the event sum. Pure function: readsModel+Msg, returns newModel+ side-effect commands.view— a single functionModel → Element. Builds widget Configs from model state; maya owns every glyph, layout decision, and animation.subscribe— registers event sources (timers, resize signals) based on current state.
The reducer has no shared mutable state, no callbacks, no event buses. Every state transition is visible in one place.
Strong ID Newtypes
All identifiers are distinct types:
ThreadId, ToolCallId, ToolName, ModelId,
CheckpointId, OAuthCode, PkceVerifier
Swapping a ThreadId for a ToolCallId is a compile error, not a debugging session. This eliminates an entire class of bugs that string-typed IDs enable.
Phase State Machine
The session tracks a phase as a closed variant:
using Phase = std::variant<
phase::Idle,
phase::Streaming,
phase::AwaitingPermission,
phase::ExecutingTool
>;
Active phases carry an Active context (cancel handle, start time, retry state, live byte counters). The active_ctx() accessor replaces what would otherwise be ~60 hand-written std::get_if chains across the codebase.
maya TUI Engine
Rendering is delegated to maya, a sister header-mostly TUI engine:
- SIMD frame diffing — only changed cells are written to the terminal.
- Yoga flexbox layout — widgets declare constraints; the engine solves them.
- 69 widgets —
Turn,AgentTimeline,Composer,StatusBar,PhaseChip,Spinner, etc.
moha builds widget Config structs from Model state. maya owns every chrome glyph, layout decision, and breathing animation. The host constructs no Elements directly.
Effect-Based Permissions
Tools declare capabilities, not trust levels:
enum class Effect : uint8_t {
ReadFs = 1 << 0, // reads filesystem state
WriteFs = 1 << 1, // mutates filesystem state
Net = 1 << 2, // sends/receives network bytes
Exec = 1 << 3, // runs model-chosen subprocess
};
The permission policy is a single constexpr function with static_assert proofs:
| Write | Ask | Minimal | |
|---|---|---|---|
| Pure | Allow | Allow | Allow |
| ReadFs | Allow | Allow | Prompt |
| WriteFs | Allow | Prompt | Prompt |
| Net | Allow | Prompt | Prompt |
| Exec | Allow | Prompt | Prompt |
Change a cell and the build breaks — not a test that nobody runs.
Sandbox
bash and diagnostics execute inside bwrap (Linux) / sandbox-exec (macOS):
- Workspace directory + system libs + network are reachable
~/.ssh,/etc, other projects are read-only- Even an approved
bashcall can’tcat ~/.ssh/id_rsa
Subprocess uses posix_spawn + poll(2) with in-process SIGTERM → SIGKILL deadlines on POSIX, CreateProcessW + reader thread on Windows. No popen quoting hazards.
File writes are atomic: write + fsync + rename (POSIX) / _commit + MoveFileExW (Windows).
Parallel Tool Safety
Tools with WriteFs or Exec effects demand exclusive access. The scheduling rule is derived from the capability model:
constexpr bool is_parallel_safe(EffectSet active, EffectSet want) noexcept;
- ReadFs + ReadFs — safe (read-read never races)
- ReadFs + Net — safe
- WriteFs + anything — serialized
- Exec + anything — serialized (model controls what runs)
This is verified at compile time with static_assert.
Source Tree
moha/
├── include/moha/
│ ├── domain/ # Model, Session, Thread, Message, Profile
│ ├── runtime/
│ │ ├── app/ # Program (init, update, subscribe)
│ │ ├── model.hpp # Full application Model
│ │ ├── msg.hpp # Event sum type (Msg)
│ │ └── view/ # View layer (Model → Element)
│ │ ├── status_bar/ # PhaseChip, Sparkline, ContextGauge
│ │ └── thread/ # Turn, AgentTimeline, WelcomeScreen
│ ├── tool/ # EffectSet, Policy, Spec, Registry
│ └── provider/ # Claude API, OAuth, streaming
├── src/ # Implementation files
├── maya/ # TUI engine (git submodule)
└── CMakeLists.txt # Build system (CMake 3.28+)
Build Requirements
- Compiler: GCC 14+ or Clang 18+
- Build system: CMake 3.28+
- Language standard: C++26
Standalone build (-DMOHA_STANDALONE=ON) statically links OpenSSL + nghttp2 + libstdc++ + libgcc. libc stays dynamic on Linux/macOS. Pass -DMOHA_FULLY_STATIC=ON with a musl toolchain for a 100% static binary.