Nyx is a personal desktop AI chat client.
The repository is now organized as a workspace, but the default product scope
remains intentionally narrow: v1 min chat. The completed baseline is a
minimal, real, streaming desktop chat loop, not a general AI workbench.
Nyx is split into two first-class subprojects:
apps/desktop: the current Electron desktop app.runtime/ocaml: an independent OCaml runtime core skeleton.
The desktop app is the only user-facing product surface right now. The OCaml
runtime exists as a foundation for later typed Agent/runtime work. Electron
main now uses the runtime-backed chat state reducer by default for the desktop
chat path, with NYX_RUNTIME_CHAT_STATE=0 reserved as a diagnostic disable.
Current architecture notes:
The current desktop milestone has a real, manually verified v1 min chat loop:
- redacted provider setup status in the renderer
- OpenAI-compatible provider streaming through Electron main
- Connections profiles with main-owned encrypted credentials and
.envfallback - one main-only provider compatibility path for generic text, GLM-style reasoning activity, explicit finish reasons, and safe stream failures
- plain-text chat with
Stop,Retry, andNew thread - unit coverage for chat reducer lifecycle, provider status parsing, provider streaming helpers, and chat presenter helpers
- real provider runthrough recorded in docs/next/llm-chat-runthrough.md
Milestone details and code entry points are recorded in docs/next/desktop-chat-milestone.md.
Important boundary: the renderer still does not read environment variables, provider tokens, full provider URLs, or raw provider configs. Provider calls and cancellation handles stay in Electron main. The OCaml runtime owns only the main-side chat state reducer semantics for this path; it does not call providers, read provider env, own credentials, or talk to the renderer.
Electron main also owns one versioned local current-thread record. Renderer state remains an in-memory projection loaded through a safe typed snapshot, and the OCaml runtime remains a rebuildable semantic projection that is replayed only when the next real turn starts. This is durability for the current thread, not a thread history collection.
The completed baseline source of truth is docs/v1-min-chat-implementation-plan.md. Implemented gated workstreams in docs/next/agent-workbench-task-slices.md are narrow, additive sources of truth for their exact shipped behavior. They supersede conflicting baseline statements only for those implemented additions and do not broaden unrelated work.
If PRD.md, DESIGN.md, or older background docs disagree with the min-chat plan, follow the min-chat plan.
In scope:
- single-page desktop chat UI
- plain text messages
- real model traffic through the Electron main process
- real streaming output
- one durable current multi-turn conversation
- complete app restart recovery for that current thread
StopRetryNew thread- explicit provider/model target selection in the Composer
- environment-based provider configuration
- provider secrets kept in Electron main only
Out of scope for this phase:
- Recent, thread switching, and persistent multi-thread history
- settings UI
- model routing or picker UI beyond the bounded Composer target selector
- Markdown or code highlighting
- tools
- agents
- plugins
- artifacts
- cloud sync
- multimodal features
Explicit agent-workbench workstreams are tracked in
docs/next/agent-workbench-task-slices.md.
They applied only when the user requested their named slices. Ordinary work
still follows the v1 min chat source of truth above.
The first agent-workbench workstream added only the foundation needed for a thread-first shell and local provider setup:
- Connections settings for OpenAI-compatible provider profiles
- encrypted local API key storage owned by Electron main
- default provider/model target resolution
.envprovider configuration as a development fallback- redacted connection status
- real provider test and model refresh
- thread-first UI copy and renderer-local thread item adapter
Settings may handle non-secret provider profile metadata through typed Connections APIs. Provider tokens and stored secrets remain main-owned, and the main chat surface stays redacted.
It still does not implement tools, MCP, terminal execution, browser automation, permission approval cards, artifacts, persistent thread history, projects/file context, thread IPC, or OCaml thread runtime wiring.
The implemented second gated workstream behavior adds only current-thread durability:
- Electron main owns one plaintext local current-thread record with owner-only file permissions
- completed, cancelled, and failed terminal state can be restored after a full app restart
- an abandoned pending turn restores as a safe retryable interrupted failure
- New thread clears the runtime projections and durable record before renderer state is cleared
- malformed storage fails closed and remains untouched until explicit New thread/Start fresh
It does not add Recent, thread switching, a hidden history collection, conversation encryption, or an OCaml Thread domain.
The completed third gated workstream adds only a main-owned provider compatibility core:
- explicit provider identity and protocol in the resolved chat target
- the existing generic OpenAI-compatible request mapping as a pure function
- normalized text, reasoning-activity, finish, and stream-error events
- deterministic reasoning-only, empty-final, and output-length failures
- preservation of a partial failed draft for the existing Retry path
- redacted generic, Ark-compatible, and GLM-style stream fixtures
It handles output exhaustion safely but does not prevent it. It does not add provider-specific request parameters, an adapter registry, capability profiles, Connections schema changes, new UI or IPC, raw reasoning exposure, tools, or native protocol adapters.
The implemented D1-D4 slices of the fourth gated workstream add only bounded Composer target selection:
- a redacted catalog of selectable saved targets and the configured
.envfallback - one renderer-local unsent target draft and one explicit target selection on each Send or Retry
- Electron-main validation, resolution, and durable target binding with no silent fallback
- a version-2 current-thread record with committed selection and safe per-response attribution
- compact target selection and assistant attribution UI
- deterministic target hydration, refresh, Retry, New thread, unavailable, and active-generation behavior
The required automated acceptance passes. The interactive two-target provider, streaming switch, failure/recovery, and restart matrix remains pending and is tracked without overclaiming in composer-target-selection-runthrough.md. This workstream does not add automatic routing, capability profiles, provider-specific parameters, attempt history, persistent multi-thread history, new runtime protocol fields, or a second durable selection owner.
The product direction for these gated workstreams is recorded in agent-workbench-direction.md.
apps/desktop owns:
- Electron main, preload, and renderer
- desktop UI
- current provider integration
- environment variables and provider credentials
- OS-facing side effects
- current
v1 min chatbehavior - the one durable current-thread record and its recovery/reset lifecycle
runtime/ocaml owns:
- runtime domain types
- runtime event model
- future state transitions
- future tool scheduling semantics
- future policy and capability model
- replayable runtime tests
Electron main is the only desktop process that may communicate with OCaml, over
stdio/NDJSON. Current desktop use is limited to runtime health/protocol
verification and the default runtime-backed chat state path in Electron main.
Set NYX_RUNTIME_CHAT_STATE=0 only for diagnostic fallback. The renderer must
never talk to the OCaml runtime directly.
Root tooling is managed through mise tasks. mise manages Node and pnpm for the workspace, packageManager keeps Corepack-based environments such as CI on the same pnpm version, and opam is expected to be available on the machine.
Initial setup:
mise install
pnpm install
mise run runtime:setupThe runtime setup creates a local opam switch under:
runtime/ocaml/_opam
Do not commit generated directories such as node_modules, out, dist, _build, or _opam.
Local provider credentials are kept in a root .env file, using
.env.example as the template:
NYX_API_BASE_URL=
NYX_API_TOKEN=
NYX_MODEL=
# Optional diagnostic override:
# NYX_RUNTIME_CHAT_STATE=0The root .env file is ignored by Git. mise run desktop:dev automatically
loads it before starting the desktop app, so the renderer still does not read
environment variables or receive provider secrets. Unless
NYX_RUNTIME_CHAT_STATE=0 is set for diagnostics, the dev task also prepares
the local OCaml runtime install output before launching Electron.
Workspace:
mise run check
mise run build
mise run format
mise run format-checkDesktop:
mise run desktop:dev
mise run desktop:build
mise run desktop:typecheck
mise run desktop:typecheck:compat
mise run desktop:lint
mise run desktop:format
mise run desktop:format-check
mise run desktop:checkOCaml runtime:
mise run runtime:setup
mise run runtime:build
mise run runtime:test
mise run runtime:format
mise run runtime:format-check
mise run runtime:ping
mise run runtime:check
mise run runtime:chat-state:checkRoot pnpm scripts are compatibility aliases for the same mise tasks:
pnpm dev
pnpm build
pnpm check
pnpm format
pnpm format:check
pnpm lint
pnpm typecheck
pnpm typecheck:compatFor desktop-only changes:
mise run desktop:typecheck
mise run desktop:typecheck:compat
mise run desktop:lintFor runtime-only changes:
mise run runtime:build
mise run runtime:test
mise run runtime:format-checkFor cross-boundary, tooling, or structural changes:
mise run check
mise run build- Keep the product inside
v1 min chatunless a task explicitly changes scope. - For explicit agent-workbench workstream tasks, follow agent-workbench-task-slices.md for that slice only.
- Do not implement new or broader Electron <-> OCaml communication as part of structural docs or setup work.
- Renderer code must not read environment variables or provider credentials.
- Provider calls and cancellation handles belong in Electron main.
- Runtime code should stay pure and tiny until a concrete runtime behavior requires more.
- Do not add tools, agents, plugin UI, broader persistence, or settings UI outside an explicit agent-workbench slice that allows it.