Contents (10)

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 closed std::variant of every event that can happen: key press, API delta, tool completion, timer tick, etc.
  • update — a single std::visit over the event sum. Pure function: reads Model + Msg, returns new Model + side-effect commands.
  • view — a single function Model → 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 widgetsTurn, 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:

WriteAskMinimal
PureAllowAllowAllow
ReadFsAllowAllowPrompt
WriteFsAllowPromptPrompt
NetAllowPromptPrompt
ExecAllowPromptPrompt

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 bash call can’t cat ~/.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.