feat(cli): add EverOS demo TUI (#298)

* feat: add EverOS demo TUI

* docs: add EverOS demo quick start

* style(cli): elevate EverOS demo TUI

* style(cli): align demo palette with poster

* fix(cli): round demo sphere animation loop

* fix(cli): refine EverOS demo TUI layout

* fix(cli): use uniform demo sphere dots

* fix(cli): refine EverOS demo sphere media

* fix(docs): smooth EverOS demo animation

* fix(docs): raise demo animation frame rate

* docs(readme): note external demo animation hosting

* fix(docs): optimize demo animation size

* feat(cli): add demo confetti success state

* feat(cli): make demo playable

* test(cli): normalize demo help ansi output

* docs(readme): streamline demo and roadmap sections

* docs(readme): restore star history section

* refactor(tui): move demo implementation out of cli
This commit is contained in:
Elliot Chen 2026-06-23 18:33:44 +08:00 committed by GitHub
parent dbfe3483f5
commit 32cb22343d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1816 additions and 178 deletions

243
README.md
View File

@ -21,16 +21,12 @@
<br>
- [EverOS 1.0.0](#everos-100)
- [Upcoming Features](#upcoming-features)
- [Why Ever OS](#why-ever-os)
- [Quick Start](#quick-start)
- [Use Cases](#use-cases)
- [Architecture At A Glance](#architecture-at-a-glance)
- [Storage Layout](#storage-layout)
- [Features](#features)
- [Project Structure](#project-structure)
- [Documentation](#documentation)
- [Watch EverOS](#watch-everos)
- [Star Us](#star-us)
- [EverMind Ecosystems](#evermind-ecosystems)
- [Contributing](#contributing)
@ -39,19 +35,16 @@
</details>
## EverOS 1.0.0
## Upcoming Features
> [!IMPORTANT]
>
> **EverOS 1.0.0 is a major release for self-evolving memory.** It brings a
> local-first runtime, Markdown as the source of truth, hybrid retrieval,
> multimodal ingestion, user and agent memory scopes, and modular algorithms
> through [EverAlgo](https://github.com/EverMind-AI/EverAlgo).
> **Knowledge Wiki:** editable, source-backed Markdown knowledge pages built
> from memory.
>
> **Coming next:** Knowledge Wiki will turn memory into editable,
> source-backed Markdown knowledge pages. Reflection will run when the
> system is idle or offline to connect signals, compress
> history, and improve profiles and skills between sessions.
> **Reflection:** idle-time and offline memory evolution that connects
> signals, compresses history, and improves profiles and skills between
> sessions.
<br>
<div align="right">
@ -63,11 +56,11 @@
## Why Ever OS
EverOS is the local memory operating system for agents and makers. It gives
one portable memory layer across coding assistants, apps, devices, and
workflows. Today it stores conversations, files, and agent trajectories as
readable Markdown, then syncs local SQLite and LanceDB indexes for fast
retrieval and self-evolving reuse.
EverOS is a Python library and local-first memory runtime for agents and
makers. It gives one portable memory layer across coding assistants, apps,
devices, and workflows from day one. It stores conversations, files, and agent
trajectories as readable Markdown, then syncs local SQLite and LanceDB indexes
for fast retrieval and self-evolving reuse.
<table>
<tr>
@ -100,16 +93,6 @@ retrieval and self-evolving reuse.
<td>✅ Search by <code>user_id</code>, <code>agent_id</code>, <code>app_id</code>, <code>project_id</code>, and <code>session_id</code></td>
<td>❌ Usually app, namespace, tenant, thread, or graph scoped</td>
</tr>
<tr>
<td><strong>Knowledge Wiki</strong></td>
<td>✅ Coming next: editable, source-backed Markdown knowledge pages built from memory</td>
<td>❌ Usually retrieval, graph, dashboards, or generated summaries instead of editable source-backed pages</td>
</tr>
<tr>
<td><strong>Reflection</strong></td>
<td>✅ Coming next: Reflection that runs when the system is idle or offline to connect signals, compress history, and improve profiles and skills between sessions</td>
<td>❌ Usually online read/write APIs, retrieval records, or summaries rather than idle-time memory consolidation</td>
</tr>
</table>
<br>
@ -122,7 +105,8 @@ retrieval and self-evolving reuse.
## Quick Start
> Goal: start EverOS, write one memory, and search it back.
> Goal: play with the memory visualizer first, then start EverOS, write one
> real memory, and search it back.
### 0. Prerequisites
@ -138,7 +122,43 @@ uv pip install everos
# or: pip install everos
```
### 2. Configure
### 2. Play With The Demo
Run this before configuring API keys or starting the server:
```bash
everos demo
```
The command asks for one memory and one recall question, then opens a
full-screen terminal UI. This is an educational visualizer: it is hardcoded,
local to the CLI, and does not connect to the EverOS server. Its job is to make
the memory lifecycle visible: conversation -> memory sphere -> recall -> source
proof -> confetti. See [docs/everos-demo.md](docs/everos-demo.md) for the demo
scope and TUI source layout.
The sphere moves through ingest, extraction, indexing, recall, source reveal,
and a confetti burst after the first memory lands. Press `r` to replay and `q`
to quit. The optimized animated preview is externally hosted so the repository
stays light:
<p align="center">
<img src="https://gist.githubusercontent.com/cyfyifanchen/afa2cf40bf138a3ec96d917e8f2791a2/raw/d4ce82a6ddd7b3ebaf221e4825af993aeca5a7ce/everos-demo-tui-animation.svg" alt="Animated EverOS demo preview showing the memory sphere moving through recall and confetti states" width="720">
</p>
For the looping showroom view used in README media, run:
```bash
everos demo --cinematic
```
If your shell is not interactive, or you want a copyable preview, use:
```bash
everos demo --plain
```
### 3. Configure
Generate a starter `.env` file, then fill the four API key slots shown in the
generated comments. Only two distinct keys are needed with the defaults:
@ -153,7 +173,7 @@ cp .env.example .env
`everos init` writes `./.env` by default. Use `everos init --xdg` to
write `${XDG_CONFIG_HOME:-~/.config}/everos/.env` instead.
### 3. Start EverOS
### 4. Start EverOS
```bash
everos server start
@ -177,7 +197,7 @@ The endpoint stack is OpenAI-protocol compatible (OpenAI / OpenRouter / vLLM /
Ollama / DeepInfra) - override `*__BASE_URL` in the generated `.env` to point
at any of them.
### 4. Try Your First Memory
### 5. Try Your First Memory
Add a tiny conversation:
@ -541,7 +561,7 @@ A context-native AI wearable that listens to everyday life and converts conversa
#### Legacy OpenClaw Agent Memory
Archived pre-1.0.0 plugin reference. New integrations should use the EverOS 1.0.0 API.
Archived pre-1.0.0 plugin reference. New integrations should use the current EverOS API.
[Learn more](docs/migration-to-1.0.0.md)
@ -615,115 +635,15 @@ Explore stored entities and relationships in a graph interface. Frontend demo; b
</div>
## Architecture At A Glance
```
┌───────────────────────────────────────────────┐
│ entrypoints/ (CLI + HTTP API) │ presentation
├───────────────────────────────────────────────┤
│ service/ (use cases: memorize/retrieve) │ application
├───────────────────────────────────────────────┤
│ memory/ (extract + search + cascade) │ domain
├───────────────────────────────────────────────┤
│ infra/ (markdown / sqlite / lancedb) │ infrastructure
└───────────────────────────────────────────────┘
↑ ↑
component/ core/
(LLM/Embedding) (observability/lifespan)
```
DDD 5 layers, single-direction dependency. See [docs/architecture.md](docs/architecture.md).
<br>
<div align="right">
[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
</div>
## Storage Layout
```
~/.everos/
├── default_app/ # app_id ("default" → "default_app" on disk)
│ └── default_project/ # project_id ("default" → "default_project")
│ ├── users/<user_id>/
│ │ ├── user.md # profile
│ │ ├── episodes/ # daily-log episodes (visible)
│ │ ├── .atomic_facts/ # nested facts (dotfile-hidden)
│ │ └── .foresights/ # predictive memory (dotfile-hidden)
│ └── agents/<agent_id>/
│ ├── agent.md
│ ├── .cases/ # one task case per entry
│ └── skills/ # named procedural memories
├── .index/ # derived indexes (rebuildable from md)
│ ├── sqlite/system.db # state + queue + audit
│ └── lancedb/*.lance/ # vector + BM25 + scalar
└── .tmp/ # transient working files
```
Open any `<app>/<project>/users/<user_id>/` folder in Obsidian — your
agent's brain is just files. The dotfile directories (`.atomic_facts/`,
`.foresights/`, `.cases/`) stay hidden by default so the visible folder
is the user-facing memory surface, while extracted derivatives sit
quietly alongside.
<br>
<div align="right">
[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
</div>
## Features
- **Hybrid retrieval**: BM25 + cosine vector ANN + scalar filters, backed by LanceDB
- **Cascade index sync**: edit a `.md` → file watcher → entry-level diff → LanceDB sync, sub-second
- **Multi-source extraction**: conversations / agent trajectories / file knowledge
- **Dual-track memory**: user-track (Episodes / Profiles) + agent-track (Cases / Skills)
- **Async-first**: full asyncio, single event loop
- **Multi-modal**: text + small image / audio inline; large media via S3/OSS reference
<br>
<div align="right">
[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
</div>
## Project Structure
```
everos/ # repo root
├── src/everos/ # main package (src layout)
│ ├── entrypoints/ # cli + api
│ ├── service/ # use case orchestration
│ ├── memory/ # domain: extract + search + cascade + prompt_slots
│ ├── infra/ # storage: markdown + lancedb + sqlite
│ ├── component/ # cross-cutting: llm / embedding / config / utils
│ ├── core/ # runtime: observability / lifespan / context
│ └── config/ # configuration data + Settings schema
├── tests/ # unit / integration / golden / fixtures
├── docs/ # design docs
└── .claude/ # team-shared rules + skills (auto-loaded by Claude Code)
```
<br>
<div align="right">
[![](https://img.shields.io/badge/-Back_to_top-gray?style=flat-square)](#readme-top)
</div>
## Documentation
- [docs/overview.md](docs/overview.md) — Project overview & vision
- [docs/architecture.md](docs/architecture.md) — DDD layered architecture & dependency rules
- [docs/engineering.md](docs/engineering.md) — Engineering & dev-efficiency infrastructure (CI / tooling / Claude Code)
- [docs/everos-demo.md](docs/everos-demo.md) — Demo scope and TUI source layout
- [docs/how-memory-works.md](docs/how-memory-works.md) — Markdown, SQLite, LanceDB, and recall flow
- [docs/use-cases.md](docs/use-cases.md) — Full use-case gallery and integration examples
- [docs/migration-to-1.0.0.md](docs/migration-to-1.0.0.md) — Legacy API and infrastructure migration notes
- [docs/engineering.md](docs/engineering.md) — Engineering and CI tooling
- [docs/migration-to-1.0.0.md](docs/migration-to-1.0.0.md) — Legacy API migration notes
- [CHANGELOG.md](CHANGELOG.md) — Release notes
- [CONTRIBUTING.md](CONTRIBUTING.md) — How to contribute
- [.claude/rules/](.claude/rules/) — Detailed coding conventions (auto-loaded by Claude Code)
<br>
<div align="right">
@ -732,42 +652,11 @@ everos/ # repo root
</div>
## Star Us
## Watch EverOS
EverOS 1.0.0 is the first release of a larger memory-system roadmap.
Watch this repository for upcoming work on deeper idle-time and offline evolution,
benchmark releases, and more real-world agent integrations.
<table>
<tr>
<td width="50%" valign="top">
<strong>Knowledge Wiki</strong><br>
<br>
Turns scattered episodes, files, facts, and agent traces into source-backed
Markdown pages for people, projects, topics, decisions, and workflows. Memory
becomes something users can read, correct, link, version, and open in their
existing Markdown tools.
</td>
<td width="50%" valign="top">
<strong>Reflection</strong><br>
<br>
Runs when the system is idle or offline to revisit stored memory, connect weak
signals, compress noisy history into durable patterns, and improve profiles and
skills. The agent gets better between active sessions, not only while you prompt
it.
</td>
</tr>
</table>
Most memory systems stop at chat history, opaque profiles, or vector recall.
EverOS keeps memory local, Markdown-native, auditable, and self-evolving: raw
memory stays readable, derived knowledge becomes a wiki, and Reflection turns
repeated experience into more useful long-term behavior.
If EverOS is useful to your agent stack, starring the repo helps more
builders discover it.
If EverOS is useful to your agent stack, please star the repo. It helps more
builders discover the project and gives the memory ecosystem a stronger signal
to keep improving.
### Star History
@ -789,7 +678,7 @@ EverMind is an open-source ecosystem for long-term memory, self-evolving agents,
<th colspan="2">EverMind Open-Source Ecosystem</th>
</tr>
<tr>
<td><strong>Core Memory Architecture</strong></td>
<td><strong>Memory Runtime</strong></td>
<td><a href="https://github.com/EverMind-AI/EverOS">EverOS</a> - the local memory operating system and research-backed runtime for agent and user memory.</td>
</tr>
<tr>
@ -797,7 +686,7 @@ EverMind is an open-source ecosystem for long-term memory, self-evolving agents,
<td><a href="https://github.com/EverMind-AI/EverAlgo">EverAlgo</a> - stateless extraction, ranking, parsing, and memory operators that power EverOS.</td>
</tr>
<tr>
<td><strong>Alternative Architecture</strong></td>
<td><strong>Hypergraph Memory</strong></td>
<td><a href="https://github.com/EverMind-AI/HyperMem">HyperMem</a> - hypergraph memory for long-term conversations, with its own benchmark-backed topic -> episode -> fact retrieval method.</td>
</tr>
<tr>
@ -831,7 +720,7 @@ Together, these repositories form EverMind's research-to-runtime stack: new memo
## Contributing
Contributions are welcome across the whole repository: architecture methods, benchmark coverage, use-case examples, documentation, and bug fixes. Browse [Issues](https://github.com/EverMind-AI/EverOS/issues) to find a good entry point, then open a PR when you are ready.
Contributions are welcome across the whole repository: memory methods, benchmark coverage, use-case examples, documentation, and bug fixes. Browse [Issues](https://github.com/EverMind-AI/EverOS/issues) to find a good entry point, then open a PR when you are ready.
<br>

51
docs/everos-demo.md Normal file
View File

@ -0,0 +1,51 @@
# EverOS Demo
`everos demo` is a local educational TUI. It helps new users feel the memory
lifecycle before they configure API keys, start the server, or write real
memory through the API.
## Run It
```bash
everos demo
```
The command asks for one memory and one recall question, then opens a
full-screen terminal UI. The visual flow is deterministic and local to the CLI:
conversation -> memory sphere -> recall -> source proof -> confetti.
For non-interactive shells or a copyable preview, use:
```bash
everos demo --plain
```
For the looping showroom view used by README media, use:
```bash
everos demo --cinematic
```
## What It Does Not Do
The demo does not connect to the EverOS server, call LLM providers, or write
production memory files. It is intentionally hardcoded so users can try the
experience before configuring the full runtime.
## Source Layout
The CLI command adapter stays under `src/everos/entrypoints/cli/commands/demo.py`
because the public command is still `everos demo`.
The TUI implementation lives under `src/everos/entrypoints/tui/demo/`:
- `app.py` renders the Textual app.
- `data.py` builds the deterministic demo story.
- `widgets/sphere.py` builds the memory sphere frames.
- `readme_media.py` renders README media.
To regenerate README media locally:
```bash
uv run python -m everos.entrypoints.tui.demo.readme_media --out-dir /tmp/everos-demo-media
```

View File

@ -47,6 +47,7 @@ dependencies = [
# CLI
"typer>=0.12.0",
"textual>=8.2.7",
# Tokenization (BM25 Chinese support)
"jieba==0.42.1",

View File

@ -0,0 +1,108 @@
"""``everos demo`` — first-run memory sphere demo."""
from __future__ import annotations
import sys
import typer
from rich.console import Console
from rich.panel import Panel
from everos.entrypoints.tui.demo.data import (
DEFAULT_MEMORY_SEED,
DEFAULT_QUERY,
DemoStory,
build_demo_story,
default_demo_story,
)
from everos.entrypoints.tui.demo.widgets.sphere import (
EVEROS_GREEN,
EVEROS_YELLOW,
build_dot_sphere,
render_dot_sphere_text,
)
def register(parent: typer.Typer) -> None:
"""Attach the ``demo`` command to the root CLI app."""
@parent.command("demo")
def demo(
plain: bool = typer.Option(
False,
"--plain",
help="Print a static terminal preview instead of launching the TUI.",
),
cinematic: bool = typer.Option(
False,
"--cinematic",
help="Skip prompts and launch the looping README-style demo.",
),
) -> None:
"""Launch the EverOS first-memory Textual TUI."""
if plain or not sys.stdout.isatty():
_print_plain_demo()
return
_run_interactive_demo(cinematic=cinematic)
def _run_interactive_demo(*, cinematic: bool) -> None:
run_demo_tui = _load_run_demo_tui()
story = None if cinematic else _collect_playable_story()
run_demo_tui(story=story)
def _load_run_demo_tui():
try:
from everos.entrypoints.tui.demo.app import run_demo_tui
except ModuleNotFoundError as exc:
if exc.name != "textual":
raise
typer.secho(
"error: Textual is required for `everos demo`; install the "
"package with TUI dependencies or run `everos demo --plain`.",
fg=typer.colors.RED,
err=True,
)
raise typer.Exit(code=1) from exc
return run_demo_tui
def _collect_playable_story() -> DemoStory:
Console().print(
f"[bold {EVEROS_YELLOW}]EverOS demo[/] "
"Give it one memory, then ask for it back."
)
memory = typer.prompt(
"Give EverOS one thing to remember",
default=DEFAULT_MEMORY_SEED,
)
query = typer.prompt(
"Ask EverOS to recall it",
default=DEFAULT_QUERY,
)
return build_demo_story(memory, query)
def _print_plain_demo(story: DemoStory | None = None) -> None:
story = story or default_demo_story()
console = Console()
frame = build_dot_sphere(
width=57,
height=23,
phase=0.18,
state_key="remembered",
)
console.print(
Panel(
render_dot_sphere_text(frame),
title="EverOS Memory Sphere",
border_style=EVEROS_YELLOW,
)
)
console.print(f"[bold {EVEROS_GREEN}]EverOS remembered:[/]")
console.print(story.memory)
console.print()
console.print(f"[bold {EVEROS_YELLOW}]Source:[/] {story.source_filename}")

View File

@ -13,7 +13,7 @@ from __future__ import annotations
import typer
from .commands import cascade, init_cmd, server
from .commands import cascade, demo, init_cmd, server
app = typer.Typer(
name="everos",
@ -28,6 +28,7 @@ app.add_typer(cascade.app, name="cascade")
# ``init`` is a top-level leaf command (not a Typer group) — match the
# idiomatic ``alembic init`` / ``django-admin startproject`` shape.
init_cmd.register(app)
demo.register(app)
if __name__ == "__main__":

View File

@ -0,0 +1 @@
"""Terminal UI entrypoints."""

View File

@ -0,0 +1 @@
"""EverOS educational demo TUI."""

View File

@ -0,0 +1,345 @@
"""Textual TUI for ``everos demo``."""
from __future__ import annotations
from rich.text import Text
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.timer import Timer
from textual.widgets import Footer, Static
from everos.entrypoints.tui.demo.data import DemoStory, default_demo_story
from everos.entrypoints.tui.demo.widgets.sphere import (
EVEROS_AMBER,
EVEROS_AMBER_DIM,
EVEROS_CYAN,
EVEROS_GREEN,
EVEROS_ORANGE,
EVEROS_YELLOW,
EVEROS_YELLOW_SOFT,
build_dot_sphere,
render_dot_sphere_text,
)
EVEROS_BLACK = "#1D1C18"
EVEROS_SURFACE = "#24231E"
EVEROS_SURFACE_RAISED = "#31302B"
EVEROS_INK = "#F5EDDC"
EVEROS_MUTED = "#918C80"
EVEROS_BORDER = "#5A5549"
SPHERE_FRAME_WIDTH = 37
SPHERE_FRAME_HEIGHT = 17
TERMINAL_CELL_HEIGHT_RATIO = 2.0
SIGNAL_RAIL_SOURCE_WIDTH = 18
class DotSphereWidget(Static):
"""Animated dot sphere that represents EverOS memory activity."""
DEFAULT_CSS = """
DotSphereWidget {
height: 1fr;
content-align: center middle;
}
"""
STATES = (
"booting",
"ingesting",
"extracting",
"indexing",
"recalling",
"remembered",
"source",
"celebrating",
)
def __init__(self) -> None:
super().__init__()
self._phase = 0.0
self._tick = 0
self._animation_timer: Timer | None = None
def on_mount(self) -> None:
self._animation_timer = self.set_interval(1 / 12, self._advance)
self._advance()
def pause_animation(self) -> None:
if self._animation_timer is not None:
self._animation_timer.pause()
def _advance(self) -> None:
self._phase = (self._phase + 0.025) % 1.0
self._tick += 1
state = self.STATES[(self._tick // 36) % len(self.STATES)]
frame = build_dot_sphere(
width=SPHERE_FRAME_WIDTH,
height=SPHERE_FRAME_HEIGHT,
phase=self._phase,
state_key=state,
)
self.update(render_dot_sphere_text(frame))
class EverOSDemoApp(App[None]):
"""Fullscreen first-run demo cockpit."""
TITLE = "EverOS Memory Core"
SUB_TITLE = "dot sphere demo"
BINDINGS = [
("q", "quit", "Quit"),
("r", "replay", "Replay"),
]
CSS = f"""
Screen {{
background: {EVEROS_BLACK};
color: {EVEROS_INK};
}}
#shell {{
width: 100%;
height: 100%;
padding: 1 2;
border: round {EVEROS_BORDER};
}}
#command-strip {{
height: 2;
padding: 0 1;
color: {EVEROS_INK};
content-align: left middle;
}}
#main {{
height: 1fr;
margin-top: 1;
}}
#memory-field {{
width: 1fr;
border: round {EVEROS_AMBER};
background: {EVEROS_SURFACE};
padding: 0 2;
}}
#field-header {{
height: 2;
content-align: left middle;
}}
#field-answer {{
height: 2;
border-top: hkey {EVEROS_AMBER_DIM};
background: {EVEROS_SURFACE_RAISED};
padding: 0 1;
}}
#signal-rail {{
width: 48;
height: 100%;
margin-left: 1;
border: round {EVEROS_AMBER};
background: {EVEROS_SURFACE};
padding: 1 2;
}}
#provenance-strip {{
height: 6;
margin-top: 1;
}}
#source-lock {{
width: 1fr;
border: round {EVEROS_CYAN};
background: {EVEROS_SURFACE};
padding: 0 2;
margin-right: 1;
}}
#recall-lock {{
width: 54;
border: round {EVEROS_GREEN};
background: {EVEROS_SURFACE};
padding: 0 2;
}}
#payoff {{
height: 2;
border-top: hkey {EVEROS_YELLOW};
background: {EVEROS_SURFACE};
color: {EVEROS_INK};
padding: 0 1;
margin-top: 1;
content-align: left middle;
}}
Footer {{
background: {EVEROS_BLACK};
color: {EVEROS_MUTED};
}}
FooterKey {{
background: {EVEROS_BLACK};
}}
FooterKey > .footer-key--key {{
color: {EVEROS_BLACK};
background: {EVEROS_YELLOW};
text-style: bold;
}}
FooterKey > .footer-key--description {{
color: {EVEROS_INK};
background: {EVEROS_BLACK};
}}
"""
def __init__(self, *, story: DemoStory | None = None) -> None:
super().__init__()
self._story = story or default_demo_story()
def compose(self) -> ComposeResult:
with Vertical(id="shell"):
yield Static(_hero_text(), id="command-strip")
with Horizontal(id="main"):
memory_field = Vertical(id="memory-field")
memory_field.border_title = "memory field"
with memory_field:
yield Static(_field_header_text(self._story), id="field-header")
yield DotSphereWidget()
yield Static(_sphere_caption(self._story), id="field-answer")
signal_rail = Static(_signal_rail_text(self._story), id="signal-rail")
signal_rail.border_title = "signal rail"
yield signal_rail
with Horizontal(id="provenance-strip"):
source_lock = Static(_source_tree_text(self._story), id="source-lock")
source_lock.border_title = "source lock"
yield source_lock
recall_lock = Static(_recall_proof_text(self._story), id="recall-lock")
recall_lock.border_title = "recall lock"
yield recall_lock
yield Static(_payoff_text(self._story), id="payoff")
yield Footer(show_command_palette=False)
def action_replay(self) -> None:
widget = self.query_one(DotSphereWidget)
widget._tick = 0
widget._phase = 0.0
widget._advance()
def run_demo_tui(*, story: DemoStory | None = None) -> None:
EverOSDemoApp(story=story).run()
def _hero_text() -> Text:
return Text.assemble(
(" everos demo ", f"bold black on {EVEROS_YELLOW}"),
(" memory core ", f"bold {EVEROS_YELLOW}"),
("online", EVEROS_MUTED),
)
def _field_header_text(story: DemoStory | None = None) -> Text:
story = story or default_demo_story()
return Text.assemble(
(f"user={story.owner}", f"bold {EVEROS_INK}"),
(" scope=local-first", f"bold {EVEROS_YELLOW_SOFT}"),
(" trace ", EVEROS_MUTED),
("conversation -> facts -> index", f"bold {EVEROS_YELLOW}"),
(" live", f"bold {EVEROS_ORANGE}"),
)
def _sphere_caption(story: DemoStory | None = None) -> Text:
story = story or default_demo_story()
return Text.assemble(
("query ", f"bold {EVEROS_CYAN}"),
(f"{story.query} ", EVEROS_INK),
("-> ", EVEROS_MUTED),
("answer ", f"bold {EVEROS_GREEN}"),
(story.answer, f"bold {EVEROS_GREEN}"),
)
def _signal_rail_text(story: DemoStory | None = None) -> Text:
story = story or default_demo_story()
return Text.assemble(
("", f"bold {EVEROS_GREEN}"),
("memory core ", EVEROS_INK),
("ready\n", f"bold {EVEROS_GREEN}"),
("", f"bold {EVEROS_YELLOW_SOFT}"),
("conversation ", EVEROS_INK),
("captured\n", f"bold {EVEROS_YELLOW_SOFT}"),
("", f"bold {EVEROS_ORANGE}"),
("episode -> facts ", EVEROS_INK),
("live\n", f"bold {EVEROS_ORANGE}"),
("", f"bold {EVEROS_CYAN}"),
("SQLite + LanceDB ", EVEROS_INK),
("synced\n", f"bold {EVEROS_CYAN}"),
("", f"bold {EVEROS_GREEN}"),
("memory recall ", EVEROS_INK),
("hit\n", f"bold {EVEROS_GREEN}"),
("\nsource route\n", EVEROS_MUTED),
(_rail_cell(story.source_filename), EVEROS_INK),
(" attached\n", f"bold {EVEROS_YELLOW_SOFT}"),
(_rail_cell(story.fact_filename), EVEROS_INK),
(" 7 nodes\n", f"bold {EVEROS_ORANGE}"),
("lancedb orbit ", EVEROS_INK),
("synced\n", f"bold {EVEROS_CYAN}"),
("\nrecall proof\n", EVEROS_MUTED),
("score ", EVEROS_INK),
("0.628\n", f"bold {EVEROS_GREEN}"),
("source ", EVEROS_INK),
(f"{story.source_filename}\n", f"bold {EVEROS_CYAN}"),
("field integrity\n", EVEROS_MUTED),
("█████████░ 92%\n", f"bold {EVEROS_YELLOW}"),
("latency ", EVEROS_MUTED),
("42 ms\n", f"bold {EVEROS_GREEN}"),
("mode ", EVEROS_MUTED),
("local-first", f"bold {EVEROS_INK}"),
)
def _rail_cell(value: str, *, width: int = SIGNAL_RAIL_SOURCE_WIDTH) -> str:
if len(value) > width:
return f"{value[: width - 3]}..."
return f"{value:<{width}}"
def _source_tree_text(story: DemoStory | None = None) -> Text:
story = story or default_demo_story()
return Text.assemble(
("episode ", EVEROS_MUTED),
(f"{story.source_filename}\n", f"bold {EVEROS_YELLOW_SOFT}"),
("facts ", EVEROS_MUTED),
(f"{story.fact_filename}\n", f"bold {EVEROS_ORANGE}"),
("index ", EVEROS_MUTED),
("sqlite/system.db + lancedb/*.lance\n", EVEROS_CYAN),
("root ", EVEROS_MUTED),
("~/.everos/default_app/default_project", EVEROS_INK),
)
def _recall_proof_text(story: DemoStory | None = None) -> Text:
story = story or default_demo_story()
return Text.assemble(
("score ", EVEROS_MUTED),
("0.628\n", f"bold {EVEROS_GREEN}"),
("scope ", EVEROS_MUTED),
(f"user={story.owner} project=default\n", EVEROS_INK),
("answer ", EVEROS_MUTED),
(story.answer, f"bold {EVEROS_YELLOW}"),
)
def _payoff_text(story: DemoStory | None = None) -> Text:
story = story or default_demo_story()
return Text.assemble(
("memory formed: ", f"bold {EVEROS_YELLOW}"),
(
f"EverOS recalled {story.answer} and kept the source attached.",
f"bold {EVEROS_INK}",
),
)

View File

@ -0,0 +1,75 @@
"""Scripted story data for the educational ``everos demo`` flow."""
from __future__ import annotations
from dataclasses import dataclass
DEFAULT_MEMORY_SEED = "I love climbing in Yosemite every spring."
DEFAULT_QUERY = "Where do I like to climb?"
@dataclass(frozen=True, slots=True)
class DemoStory:
"""Small, deterministic story rendered by the demo TUI."""
owner: str
memory: str
query: str
answer: str
source_filename: str
fact_filename: str
def default_demo_story() -> DemoStory:
"""Return the cinematic story used by README media and no-prompt previews."""
return DemoStory(
owner="alice",
memory=DEFAULT_MEMORY_SEED,
query="Where does Alice like to climb?",
answer="Yosemite every spring",
source_filename="episode-2026-06-20.md",
fact_filename="atomic_fact-2026-06-20.md",
)
def build_demo_story(
memory_seed: str | None = None,
query: str | None = None,
) -> DemoStory:
"""Build a playable demo story from one user memory and one recall query."""
memory = _clean(memory_seed, DEFAULT_MEMORY_SEED)
recall_query = _clean(query, DEFAULT_QUERY)
return DemoStory(
owner="you",
memory=memory,
query=recall_query,
answer=_derive_demo_answer(memory),
source_filename="episode-demo.md",
fact_filename="atomic_fact-demo.md",
)
def _clean(value: str | None, fallback: str) -> str:
if value is None:
return fallback
stripped = value.strip()
return stripped or fallback
def _derive_demo_answer(memory: str) -> str:
"""Keep the demo deterministic without pretending to run the server."""
lower_memory = memory.lower()
if "yosemite" in lower_memory:
if "spring" in lower_memory:
return "Yosemite every spring"
return "Yosemite"
return _compact(memory)
def _compact(text: str, *, limit: int = 66) -> str:
if len(text) <= limit:
return text
return f"{text[: limit - 3].rstrip()}..."

View File

@ -0,0 +1,267 @@
"""Render README media for the ``everos demo`` TUI."""
from __future__ import annotations
import argparse
import asyncio
import base64
import html
import io
import os
import re
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
import anyio
from rich.console import Console
from rich.terminal_theme import TerminalTheme
TERMINAL_SIZE = (150, 48)
ANIMATION_TERMINAL_SIZE = (132, 42)
ANIMATION_FPS = 24
ANIMATION_FRAMES_PER_STATE = 4
FRAME_SECONDS = 1 / ANIMATION_FPS
EVEROS_TERMINAL_THEME = TerminalTheme(
background=(29, 28, 24),
foreground=(245, 237, 220),
normal=[
(29, 28, 24),
(192, 149, 37),
(216, 205, 175),
(249, 185, 28),
(139, 118, 63),
(246, 194, 59),
(245, 237, 220),
(245, 237, 220),
],
bright=[
(49, 48, 43),
(192, 149, 37),
(216, 205, 175),
(249, 185, 28),
(145, 140, 128),
(246, 194, 59),
(245, 237, 220),
(245, 237, 220),
],
)
@dataclass(frozen=True)
class FramePlan:
state: str
phase: float
@dataclass(frozen=True)
class OpacitySchedule:
key_times: str
values: str
def build_frame_plan(
states: Sequence[str],
*,
frames_per_state: int = ANIMATION_FRAMES_PER_STATE,
) -> tuple[FramePlan, ...]:
"""Return animation frames, closed by a duplicate of the first frame."""
if not states:
raise ValueError("at least one animation state is required")
if frames_per_state < 1:
raise ValueError("frames_per_state must be at least 1")
state_count = len(states)
frame_count = state_count * frames_per_state
frames = [
FramePlan(
state=states[index // frames_per_state],
phase=index / frame_count,
)
for index in range(frame_count)
]
frames.append(FramePlan(state=states[0], phase=0.0))
return tuple(frames)
def opacity_schedule(*, index: int, total: int) -> OpacitySchedule:
"""Build a gap-free discrete opacity schedule for one frame."""
if total < 2:
raise ValueError("at least two frames are required")
if index < 0 or index >= total:
raise ValueError("frame index is out of range")
start = index / total
end = (index + 1) / total
if index == 0:
return OpacitySchedule(key_times=f"0;{_time(end)};1", values="1;0;0")
if index == total - 1:
return OpacitySchedule(key_times=f"0;{_time(start)};1", values="0;1;1")
return OpacitySchedule(
key_times=f"0;{_time(start)};{_time(end)};1",
values="0;1;0;0",
)
async def render_media(out_dir: Path) -> tuple[Path, Path]:
"""Render the static screenshot and looping animated SVG."""
from everos.entrypoints.tui.demo.app import DotSphereWidget
await anyio.Path(out_dir).mkdir(parents=True, exist_ok=True)
screenshot = out_dir / "everos-demo-tui-screenshot.svg"
remembered_index = DotSphereWidget.STATES.index("remembered")
remembered = FramePlan(
state=DotSphereWidget.STATES[remembered_index],
phase=remembered_index / len(DotSphereWidget.STATES),
)
await _export_frame(screenshot, remembered, terminal_size=TERMINAL_SIZE)
plan = build_frame_plan(DotSphereWidget.STATES)
frame_paths: list[Path] = []
for index, frame in enumerate(plan):
frame_path = out_dir / f"frame-{index:02d}-{frame.state}.svg"
await _export_frame(
frame_path,
frame,
terminal_size=ANIMATION_TERMINAL_SIZE,
)
frame_paths.append(frame_path)
animation = out_dir / "everos-demo-tui-animation.svg"
animation.write_text(_build_animation_svg(frame_paths, plan))
return screenshot, animation
async def _export_frame(
path: Path,
frame: FramePlan,
*,
terminal_size: tuple[int, int] = TERMINAL_SIZE,
) -> None:
from everos.entrypoints.tui.demo.app import (
SPHERE_FRAME_HEIGHT,
SPHERE_FRAME_WIDTH,
DotSphereWidget,
EverOSDemoApp,
)
from everos.entrypoints.tui.demo.widgets.sphere import (
build_dot_sphere,
render_dot_sphere_text,
)
no_color = os.environ.pop("NO_COLOR", None)
try:
app = EverOSDemoApp()
async with app.run_test(size=terminal_size) as pilot:
await pilot.pause(0.05)
widget = app.query_one(DotSphereWidget)
widget.pause_animation()
sphere = build_dot_sphere(
width=SPHERE_FRAME_WIDTH,
height=SPHERE_FRAME_HEIGHT,
phase=frame.phase,
state_key=frame.state,
)
widget.update(render_dot_sphere_text(sphere))
await pilot.pause(0.05)
screenshot = _export_screenshot_svg(app)
finally:
if no_color is not None:
os.environ["NO_COLOR"] = no_color
await anyio.Path(path).write_text(normalize_svg_terminal_ids(screenshot))
def _export_screenshot_svg(app) -> str:
width, height = app.size
console = Console(
width=width,
height=height,
file=io.StringIO(),
force_terminal=True,
color_system="truecolor",
record=True,
legacy_windows=False,
safe_box=False,
)
screen_render = app.screen._compositor.render_update(
full=True,
screen_stack=app.app._background_screens,
simplify=False,
)
console.print(screen_render)
return console.export_svg(
title=app.title,
theme=EVEROS_TERMINAL_THEME,
unique_id="terminal-everos",
)
def _build_animation_svg(frame_paths: Sequence[Path], plan: Sequence[FramePlan]) -> str:
if len(frame_paths) != len(plan):
raise ValueError("frame paths and frame plan lengths must match")
view_box, width, height = _read_svg_dimensions(frame_paths[0])
duration = len(frame_paths) * FRAME_SECONDS
images = []
for index, frame_path in enumerate(frame_paths):
encoded = base64.b64encode(frame_path.read_bytes()).decode("ascii")
schedule = opacity_schedule(index=index, total=len(frame_paths))
state = html.escape(plan[index].state)
images.append(
f' <image width="{width}" height="{height}" '
f'href="data:image/svg+xml;base64,{encoded}" opacity="0">\n'
f" <title>EverOS demo state: {state}</title>\n"
f' <animate attributeName="opacity" dur="{duration:.2f}s" '
f'repeatCount="indefinite" keyTimes="{schedule.key_times}" '
f'values="{schedule.values}" calcMode="discrete" />\n'
" </image>"
)
return (
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" '
f'height="{height}" viewBox="{view_box}">\n'
"<title>Animated EverOS demo terminal UI</title>\n"
"<desc>EverOS demo cycles through memory states with a closed loop.</desc>\n"
+ "\n".join(images)
+ "\n</svg>\n"
)
def _read_svg_dimensions(path: Path) -> tuple[str, str, str]:
svg_open = re.search(r"<svg\s+([^>]+)>", path.read_text())
if svg_open is None:
raise ValueError(f"could not find SVG root in {path}")
view_box_match = re.search(r'viewBox="([^"]+)"', svg_open.group(0))
if view_box_match is None:
raise ValueError(f"could not find SVG viewBox in {path}")
view_box = view_box_match.group(1)
_, _, width, height = view_box.split()
return view_box, width, height
def normalize_svg_terminal_ids(svg: str) -> str:
"""Normalize Rich's random terminal SVG prefix for deterministic media."""
return re.sub(r"terminal-\d+", "terminal-everos", svg)
def _time(value: float) -> str:
return f"{value:.6f}".rstrip("0").rstrip(".")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--out-dir",
type=Path,
default=Path("/tmp/everos-demo-media"),
help="Directory for generated README SVG media.",
)
args = parser.parse_args()
screenshot, animation = asyncio.run(render_media(args.out_dir))
print(screenshot)
print(animation)
if __name__ == "__main__":
main()

View File

@ -0,0 +1 @@
"""Widgets and rendering primitives for the EverOS demo TUI."""

View File

@ -0,0 +1,322 @@
"""Dot-sphere primitives for the EverOS demo TUI.
The Textual app consumes these pure rendering primitives so the animated
surface stays testable without standing up a terminal UI.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from rich.text import Text
EVEROS_YELLOW = "#F9B91C"
EVEROS_YELLOW_SOFT = "#F6C23B"
EVEROS_AMBER_DIM = "#4A3D20"
EVEROS_AMBER = "#8B763F"
EVEROS_CYAN = "#F5EDDC"
EVEROS_GREEN = "#D8CDAF"
EVEROS_ORANGE = "#C09525"
BRAILLE_BASE = 0x2800
BRAILLE_DOT_BITS = (
(0x01, 0x02, 0x04, 0x40),
(0x08, 0x10, 0x20, 0x80),
)
SPHERE_POINT_COUNT = 1300
CONFETTI_POINT_COUNT = 150
CONFETTI_GLYPHS = (".", "+", "*", "x")
CONFETTI_STYLES = (
EVEROS_YELLOW,
EVEROS_YELLOW_SOFT,
EVEROS_CYAN,
EVEROS_ORANGE,
EVEROS_AMBER,
)
GOLDEN_ANGLE = math.pi * (3 - math.sqrt(5))
@dataclass(frozen=True)
class SphereState:
"""Visual and copy settings for a sphere animation state."""
key: str
caption: str
accent: str
@dataclass(frozen=True)
class DotCell:
"""One projected dot in terminal cell coordinates."""
x: int
y: int
z: float
glyph: str
style: str
highlighted: bool = False
@dataclass(frozen=True)
class DotSphereFrame:
"""A fully projected dot-sphere frame."""
width: int
height: int
state: SphereState
cells: tuple[DotCell, ...]
@property
def caption(self) -> str:
return self.state.caption
SPHERE_STATES: dict[str, SphereState] = {
"booting": SphereState(
key="booting",
caption="forming local memory field",
accent=EVEROS_YELLOW,
),
"ingesting": SphereState(
key="ingesting",
caption="ingesting conversation dots",
accent=EVEROS_CYAN,
),
"extracting": SphereState(
key="extracting",
caption="extracting episode -> atomic facts",
accent=EVEROS_ORANGE,
),
"indexing": SphereState(
key="indexing",
caption="syncing SQLite + LanceDB orbit",
accent=EVEROS_CYAN,
),
"recalling": SphereState(
key="recalling",
caption="scanning memory sphere",
accent=EVEROS_GREEN,
),
"remembered": SphereState(
key="remembered",
caption="remembered Yosemite preference",
accent=EVEROS_YELLOW,
),
"source": SphereState(
key="source",
caption="revealing episode.md source",
accent=EVEROS_YELLOW_SOFT,
),
"celebrating": SphereState(
key="celebrating",
caption="memory crystallized",
accent=EVEROS_YELLOW,
),
}
def build_dot_sphere(
*, width: int, height: int, phase: float, state_key: str
) -> DotSphereFrame:
"""Build one dot-sphere animation frame."""
if width < 13 or height < 7:
raise ValueError("dot sphere requires at least 13x7 cells")
try:
state = SPHERE_STATES[state_key]
except KeyError as exc:
raise ValueError(f"unknown sphere state: {state_key}") from exc
if state.key == "celebrating":
return _build_confetti_burst(
width=width,
height=height,
phase=phase,
state=state,
)
sub_width = width * 2
sub_height = height * 4
sub_center_x = (sub_width - 1) / 2
sub_center_y = (sub_height - 1) / 2
radius_x = max(1.0, sub_center_x - 5)
radius_y = max(1.0, sub_center_y - 3)
rotation = phase * math.tau
active_target = _highlight_target(width, height)
masks: dict[tuple[int, int], int] = {}
depths: dict[tuple[int, int], float] = {}
highlighted_positions: set[tuple[int, int]] = set()
for index in range(SPHERE_POINT_COUNT):
y3 = 1 - 2 * ((index + 0.5) / SPHERE_POINT_COUNT)
ring_radius = math.sqrt(max(0.0, 1.0 - y3 * y3))
theta = index * GOLDEN_ANGLE + rotation
x3 = ring_radius * math.cos(theta)
z3 = ring_radius * math.sin(theta)
sub_x = round(sub_center_x + x3 * radius_x)
sub_y = round(sub_center_y + y3 * radius_y)
if not (0 <= sub_x < sub_width and 0 <= sub_y < sub_height):
continue
_add_braille_dot(
masks=masks,
depths=depths,
sub_x=sub_x,
sub_y=sub_y,
z=z3,
)
if state.key in {"recalling", "remembered", "source"}:
highlighted_positions.add(active_target)
target_sub_x = active_target[0] * 2 + 1
target_sub_y = active_target[1] * 4 + 1
_add_braille_dot(
masks=masks,
depths=depths,
sub_x=target_sub_x,
sub_y=target_sub_y,
z=1.0,
)
cells = []
for (x, y), mask in masks.items():
highlighted = (x, y) in highlighted_positions
if highlighted and state.key == "recalling":
style = EVEROS_CYAN
elif highlighted:
style = EVEROS_YELLOW
else:
style = _style_for_depth(depths[(x, y)], state)
cells.append(
DotCell(
x=x,
y=y,
z=depths[(x, y)],
glyph=chr(BRAILLE_BASE + mask),
style=style,
highlighted=highlighted,
)
)
return DotSphereFrame(
width=width,
height=height,
state=state,
cells=tuple(sorted(cells, key=lambda cell: (cell.y, cell.x))),
)
def _build_confetti_burst(
*, width: int, height: int, phase: float, state: SphereState
) -> DotSphereFrame:
center_x = (width - 1) / 2
center_y = (height - 1) / 2
radius_x = max(1.0, center_x - 3)
radius_y = max(1.0, center_y - 2)
local_phase = _state_local_phase(phase, state.key)
bloom = 0.62 + 0.58 * math.sin(local_phase * math.pi)
rotation = phase * math.tau * 1.4
cells_by_position: dict[tuple[int, int], DotCell] = {}
for index in range(CONFETTI_POINT_COUNT):
shell = 0.55 + 0.45 * ((index % 17) / 16)
angle = index * GOLDEN_ANGLE + rotation
drift = math.sin(phase * math.tau * 2 + index * 0.23)
x = round(center_x + math.cos(angle) * radius_x * shell * bloom)
y = round(
center_y
+ math.sin(angle) * radius_y * shell * bloom
+ drift * 0.75 * local_phase
)
if not (0 <= x < width and 0 <= y < height):
continue
z = math.cos(angle - rotation) * shell
glyph = CONFETTI_GLYPHS[(index + int(local_phase * 10)) % len(CONFETTI_GLYPHS)]
style = CONFETTI_STYLES[
(index * 3 + int(local_phase * 7)) % len(CONFETTI_STYLES)
]
position = (x, y)
existing = cells_by_position.get(position)
if existing is None or z > existing.z:
cells_by_position[position] = DotCell(
x=x,
y=y,
z=z,
glyph=glyph,
style=style,
)
return DotSphereFrame(
width=width,
height=height,
state=state,
cells=tuple(
sorted(cells_by_position.values(), key=lambda cell: (cell.y, cell.x))
),
)
def render_dot_sphere_lines(frame: DotSphereFrame) -> list[list[DotCell | None]]:
"""Render cells into a sparse row grid for Rich/Textual consumers."""
grid: list[list[DotCell | None]] = [
[None for _ in range(frame.width)] for _ in range(frame.height)
]
for cell in frame.cells:
if 0 <= cell.x < frame.width and 0 <= cell.y < frame.height:
grid[cell.y][cell.x] = cell
return grid
def render_dot_sphere_text(frame: DotSphereFrame) -> Text:
"""Convert a frame into styled terminal text."""
rows = render_dot_sphere_lines(frame)
text = Text(no_wrap=True)
for row in rows:
for cell in row:
if cell is None:
text.append(" ")
else:
text.append(cell.glyph, style=cell.style)
text.append("\n")
text.append("\n")
text.append(frame.caption, style=f"bold {frame.state.accent}")
return text
def _add_braille_dot(
*,
masks: dict[tuple[int, int], int],
depths: dict[tuple[int, int], float],
sub_x: int,
sub_y: int,
z: float,
) -> None:
cell_x = sub_x // 2
cell_y = sub_y // 4
local_x = sub_x % 2
local_y = sub_y % 4
position = (cell_x, cell_y)
masks[position] = masks.get(position, 0) | BRAILLE_DOT_BITS[local_x][local_y]
depths[position] = max(z, depths.get(position, -1.0))
def _style_for_depth(z: float, state: SphereState) -> str:
if state.key == "extracting" and z > 0.38:
return EVEROS_ORANGE
if state.key == "indexing" and z > 0.45:
return EVEROS_CYAN
if state.key == "ingesting" and z > 0.5:
return EVEROS_CYAN
if z > 0.58:
return EVEROS_YELLOW
if z > 0.05:
return EVEROS_YELLOW
return EVEROS_AMBER
def _highlight_target(width: int, height: int) -> tuple[int, int]:
return (round((width - 1) * 0.66), round((height - 1) * 0.42))
def _state_local_phase(phase: float, state_key: str) -> float:
state_keys = tuple(SPHERE_STATES)
return (phase * len(state_keys) - state_keys.index(state_key)) % 1.0

View File

@ -0,0 +1,106 @@
"""EverOS demo command contracts."""
from __future__ import annotations
import re
import pytest
import typer
from rich.panel import Panel
from typer.testing import CliRunner
from everos.entrypoints.cli.commands import demo as demo_command
from everos.entrypoints.tui.demo.data import build_demo_story
def test_demo_help_exposes_cinematic_mode() -> None:
app = typer.Typer()
demo_command.register(app)
result = CliRunner().invoke(app, ["demo", "--help"], terminal_width=120)
assert result.exit_code == 0
assert "--cinematic" in _strip_ansi(result.stdout)
def test_collect_playable_story_prompts_for_memory_then_query(monkeypatch) -> None:
prompts: list[tuple[str, str]] = []
replies = iter(
[
"I keep my Monday design review notes in Notion.",
"Where are my Monday review notes?",
]
)
def fake_prompt(label: str, *, default: str) -> str:
prompts.append((label, default))
return next(replies)
monkeypatch.setattr(demo_command.typer, "prompt", fake_prompt)
story = demo_command._collect_playable_story()
assert [label for label, _ in prompts] == [
"Give EverOS one thing to remember",
"Ask EverOS to recall it",
]
assert story.memory == "I keep my Monday design review notes in Notion."
assert story.query == "Where are my Monday review notes?"
def test_interactive_demo_checks_textual_before_prompt(monkeypatch) -> None:
def fail_load_tui() -> object:
raise typer.Exit(code=1)
def fail_prompt(*_: object, **__: object) -> str:
pytest.fail("demo prompted before checking TUI availability")
monkeypatch.setattr(demo_command, "_load_run_demo_tui", fail_load_tui)
monkeypatch.setattr(demo_command.typer, "prompt", fail_prompt)
with pytest.raises(typer.Exit):
demo_command._run_interactive_demo(cinematic=False)
def test_plain_demo_uses_poster_gold_brand_primary(monkeypatch) -> None:
printed: list[object] = []
class FakeConsole:
def print(self, *renderables: object, **_: object) -> None:
printed.extend(renderables)
monkeypatch.setattr(demo_command, "Console", FakeConsole)
demo_command._print_plain_demo()
panel = next(item for item in printed if isinstance(item, Panel))
printed_text = "\n".join(str(item) for item in printed)
assert panel.border_style == "#F9B91C"
assert "#F9B91C" in printed_text
assert "#FFE600" not in printed_text
def test_plain_demo_prints_custom_story(monkeypatch) -> None:
printed: list[object] = []
class FakeConsole:
def print(self, *renderables: object, **_: object) -> None:
printed.extend(renderables)
monkeypatch.setattr(demo_command, "Console", FakeConsole)
demo_command._print_plain_demo(
build_demo_story(
"I keep my Monday design review notes in Notion.",
"Where are my Monday review notes?",
)
)
printed_text = "\n".join(str(item) for item in printed)
assert "EverOS remembered" in printed_text
assert "I keep my Monday design review notes in Notion." in printed_text
assert "episode-demo.md" in printed_text
def _strip_ansi(value: str) -> str:
return re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", value)

View File

@ -13,6 +13,7 @@ def test_help_exits_zero() -> None:
assert "everos" in result.stdout
assert "server" in result.stdout
assert "cascade" in result.stdout
assert "demo" in result.stdout
def test_no_args_shows_help_and_exits_nonzero() -> None:

View File

@ -0,0 +1 @@
"""TUI entrypoint tests."""

View File

@ -0,0 +1,122 @@
"""EverOS demo TUI color contracts."""
from __future__ import annotations
import pytest
from everos.entrypoints.tui.demo.app import (
SPHERE_FRAME_HEIGHT,
SPHERE_FRAME_WIDTH,
TERMINAL_CELL_HEIGHT_RATIO,
DotSphereWidget,
EverOSDemoApp,
_field_header_text,
_hero_text,
_payoff_text,
_recall_proof_text,
_signal_rail_text,
_source_tree_text,
_sphere_caption,
)
from everos.entrypoints.tui.demo.data import build_demo_story
from everos.entrypoints.tui.demo.widgets.sphere import SPHERE_STATES
def test_demo_tui_uses_poster_derived_brand_palette() -> None:
css = EverOSDemoApp.CSS
assert "#F9B91C" in css
assert "#31302B" in css
assert "#F5EDDC" in css
assert "#918C80" in css
assert "#FFE600" not in css
assert "#55D6FF" not in css
assert "#73F7A7" not in css
assert any(span.style == "bold #F9B91C" for span in _hero_text().spans)
def test_demo_tui_uses_elevated_instrument_layout() -> None:
css = EverOSDemoApp.CSS
assert "#command-strip" in css
assert "#memory-field" in css
assert "#signal-rail" in css
assert "#provenance-strip" in css
assert "#payoff" in css
assert "FooterKey" in css
assert "background: #F9B91C" in css
assert any("on #F9B91C" in span.style for span in _hero_text().spans)
assert "border: double" not in css
assert "border: heavy" not in css
def test_demo_tui_uses_balanced_panel_proportions() -> None:
css = EverOSDemoApp.CSS
command_strip = _css_block(css, "#command-strip")
signal_rail = _css_block(css, "#signal-rail")
payoff = _css_block(css, "#payoff")
assert "height: 2;" in command_strip
assert "border-left: thick" not in command_strip
assert "background: #31302B" not in command_strip
assert len(_hero_text().plain.splitlines()) == 1
assert len(_hero_text().plain) <= 56
assert "height: 1fr;" in DotSphereWidget.DEFAULT_CSS
assert "height: 100%;" in signal_rail
assert "source route" in _signal_rail_text().plain
assert "recall proof" in _signal_rail_text().plain
assert "height: 2;" in payoff
assert "background: #24231E;" in payoff
assert "padding: 0 1;" in payoff
assert _payoff_text().plain.startswith("memory formed:")
assert "bold #F9B91C" in {span.style for span in _payoff_text().spans}
def test_demo_tui_sphere_renders_round_in_terminal_cells() -> None:
visual_ratio = (SPHERE_FRAME_WIDTH - 4) / (
SPHERE_FRAME_HEIGHT * TERMINAL_CELL_HEIGHT_RATIO
)
assert visual_ratio == pytest.approx(1.0, abs=0.04)
assert SPHERE_FRAME_WIDTH == 37
assert SPHERE_FRAME_HEIGHT == 17
def test_demo_tui_celebrates_after_source_reveal() -> None:
assert DotSphereWidget.STATES[-2:] == ("source", "celebrating")
assert set(DotSphereWidget.STATES).issubset(SPHERE_STATES)
def test_demo_tui_renders_playable_story_copy() -> None:
story = build_demo_story(
"I keep my Monday design review notes in Notion.",
"Where are my Monday review notes?",
)
assert "user=you" in _field_header_text(story).plain
assert "Where are my Monday review notes?" in _sphere_caption(story).plain
assert story.answer in _sphere_caption(story).plain
assert "server wake" not in _signal_rail_text(story).plain
assert "memory core" in _signal_rail_text(story).plain
assert story.source_filename in _source_tree_text(story).plain
assert story.fact_filename in _source_tree_text(story).plain
assert story.answer in _recall_proof_text(story).plain
assert story.answer in _payoff_text(story).plain
def test_demo_tui_signal_rail_keeps_source_status_columns_separate() -> None:
rail = _signal_rail_text().plain
assert "mdattached" not in rail
assert "md7 nodes" not in rail
assert "..." in rail
def _css_block(css: str, selector: str) -> str:
start = css.index(f"{selector} {{")
end = css.index("}", start)
return css[start:end]

View File

@ -0,0 +1,29 @@
"""EverOS playable demo story contracts."""
from __future__ import annotations
from everos.entrypoints.tui.demo.data import (
DEFAULT_MEMORY_SEED,
DEFAULT_QUERY,
build_demo_story,
)
def test_demo_story_preserves_prompted_memory_and_query() -> None:
story = build_demo_story(
"I keep my Monday design review notes in Notion.",
"Where are my Monday review notes?",
)
assert story.owner == "you"
assert story.memory == "I keep my Monday design review notes in Notion."
assert story.query == "Where are my Monday review notes?"
assert story.answer == "I keep my Monday design review notes in Notion."
assert story.source_filename == "episode-demo.md"
assert story.fact_filename == "atomic_fact-demo.md"
def test_demo_story_keeps_default_yosemite_success_moment() -> None:
story = build_demo_story(DEFAULT_MEMORY_SEED, DEFAULT_QUERY)
assert story.answer == "Yosemite every spring"

View File

@ -0,0 +1,117 @@
"""EverOS demo README media generation contracts."""
from __future__ import annotations
import html
from everos.entrypoints.tui.demo.readme_media import (
ANIMATION_FPS,
ANIMATION_FRAMES_PER_STATE,
ANIMATION_TERMINAL_SIZE,
FRAME_SECONDS,
TERMINAL_SIZE,
FramePlan,
_export_frame,
build_frame_plan,
normalize_svg_terminal_ids,
opacity_schedule,
)
def test_readme_animation_frame_plan_closes_on_first_frame() -> None:
plan = build_frame_plan(("booting", "ingesting", "source"), frames_per_state=3)
assert plan[0] == plan[-1]
assert len(plan) == 10
assert [frame.state for frame in plan] == [
"booting",
"booting",
"booting",
"ingesting",
"ingesting",
"ingesting",
"source",
"source",
"source",
"booting",
]
assert [frame.phase for frame in plan] == [
0 / 9,
1 / 9,
2 / 9,
3 / 9,
4 / 9,
5 / 9,
6 / 9,
7 / 9,
8 / 9,
0.0,
]
def test_readme_animation_default_plan_uses_smoother_frame_rate() -> None:
states = ("booting", "ingesting", "source")
plan = build_frame_plan(states)
assert len(plan) <= len(states) * 4 + 1
assert ANIMATION_FRAMES_PER_STATE == 4
assert ANIMATION_FPS >= 24
assert FRAME_SECONDS <= 1 / 24
def test_readme_animation_uses_compact_terminal_size_for_load_speed() -> None:
assert ANIMATION_TERMINAL_SIZE[0] < TERMINAL_SIZE[0]
assert ANIMATION_TERMINAL_SIZE[1] < TERMINAL_SIZE[1]
assert ANIMATION_TERMINAL_SIZE[0] * ANIMATION_TERMINAL_SIZE[1] <= 5600
def test_readme_animation_opacity_schedule_has_no_blank_gap() -> None:
schedules = [opacity_schedule(index=idx, total=4) for idx in range(4)]
assert schedules[0].values == "1;0;0"
assert schedules[-1].values == "0;1;1"
assert all("0.0005" not in schedule.key_times for schedule in schedules)
def test_readme_media_normalizes_rich_terminal_random_ids() -> None:
svg = (
"<style>.terminal-12345-matrix {}</style>"
'<clipPath id="terminal-12345-line-0"></clipPath>'
'<text class="terminal-12345-r1" '
'clip-path="url(#terminal-12345-line-0)">EverOS</text>'
)
normalized = normalize_svg_terminal_ids(svg)
assert "terminal-12345" not in normalized
assert normalized.count("terminal-everos") == 4
async def test_export_frame_freezes_animation_before_rendering_requested_state(
tmp_path, monkeypatch
) -> None:
from everos.entrypoints.tui.demo.app import DotSphereWidget
def fast_mount(self: DotSphereWidget) -> None:
self._animation_timer = self.set_interval(0.001, self._advance)
self._advance()
monkeypatch.setattr(DotSphereWidget, "on_mount", fast_mount)
path = tmp_path / "frame.svg"
await _export_frame(path, FramePlan(state="booting", phase=0.0))
svg = html.unescape(path.read_text()).replace("\xa0", " ")
assert "forming local memory field" in svg
assert "ingesting conversation dots" not in svg
async def test_export_frame_preserves_poster_palette(tmp_path) -> None:
path = tmp_path / "frame.svg"
await _export_frame(path, FramePlan(state="remembered", phase=0.5))
svg = path.read_text().lower()
assert "#f9b91c" in svg
assert "#f5eddc" in svg

View File

@ -0,0 +1,143 @@
"""EverOS demo dot-sphere rendering contracts."""
from __future__ import annotations
from everos.entrypoints.tui.demo.widgets.sphere import (
SPHERE_STATES,
DotSphereFrame,
build_dot_sphere,
)
def test_dot_sphere_forms_round_bounded_cloud() -> None:
frame = build_dot_sphere(width=41, height=19, phase=0.0, state_key="extracting")
assert frame.width == 41
assert frame.height == 19
assert frame.caption == "extracting episode -> atomic facts"
assert len(frame.cells) >= 90
center_x = (frame.width - 1) / 2
center_y = (frame.height - 1) / 2
radius_x = center_x
radius_y = center_y
for cell in frame.cells:
normalized = ((cell.x - center_x) / radius_x) ** 2 + (
(cell.y - center_y) / radius_y
) ** 2
assert normalized <= 1.08
row_counts: dict[int, int] = {}
for cell in frame.cells:
row_counts[cell.y] = row_counts.get(cell.y, 0) + 1
assert row_counts[frame.height // 2] > row_counts[min(row_counts)]
assert row_counts[frame.height // 2] > row_counts[max(row_counts)]
def test_dot_sphere_keeps_terminal_poles_visually_round() -> None:
frame = build_dot_sphere(width=37, height=17, phase=0.0, state_key="booting")
row_spans = _row_spans(frame)
assert row_spans[0] <= 8
assert row_spans[1] <= 20
assert row_spans[2] <= 26
assert row_spans[frame.height // 2] >= 31
assert row_spans[frame.height - 2] <= 20
assert row_spans[frame.height - 1] <= 8
def test_dot_sphere_uses_braille_fine_dot_cells() -> None:
for state_key in SPHERE_STATES:
if state_key == "celebrating":
continue
frame = build_dot_sphere(
width=37,
height=17,
phase=0.25,
state_key=state_key,
)
assert all(_is_braille_cell(cell.glyph) for cell in frame.cells)
assert not any(cell.glyph in {"·", "", "", ""} for cell in frame.cells)
assert not any(cell.style.startswith("bold ") for cell in frame.cells)
def test_dot_sphere_packs_multiple_subdots_per_terminal_cell() -> None:
frame = build_dot_sphere(width=37, height=17, phase=0.0, state_key="booting")
subdot_count = sum(_braille_subdot_count(cell.glyph) for cell in frame.cells)
assert subdot_count > len(frame.cells) * 1.8
assert subdot_count > frame.width * frame.height * 0.9
assert any(_braille_subdot_count(cell.glyph) >= 4 for cell in frame.cells)
def test_dot_sphere_avoids_flat_sides_in_terminal_frame() -> None:
frame = build_dot_sphere(width=37, height=17, phase=0.0, state_key="booting")
row_spans = _row_spans(frame)
assert max(row_spans.values()) <= frame.width - 4
assert row_spans[frame.height // 2] >= frame.width - 4
for y in range(frame.height // 2):
assert abs(row_spans[y] - row_spans[frame.height - 1 - y]) <= 4
def test_dot_sphere_remembered_state_has_highlighted_node() -> None:
frame = build_dot_sphere(width=41, height=19, phase=0.25, state_key="remembered")
highlighted = [cell for cell in frame.cells if cell.highlighted]
assert len(highlighted) == 1
assert _is_braille_cell(highlighted[0].glyph)
assert highlighted[0].style == "#F9B91C"
assert frame.caption == "remembered Yosemite preference"
def test_dot_sphere_celebrating_state_bursts_into_confetti() -> None:
frame = build_dot_sphere(width=41, height=19, phase=0.93, state_key="celebrating")
assert frame.caption == "memory crystallized"
confetti = [cell for cell in frame.cells if cell.glyph in {"*", "+", ".", "x"}]
assert len(confetti) >= 70
assert all(not _is_braille_cell(cell.glyph) for cell in confetti)
assert not any(cell.style.startswith("bold ") for cell in confetti)
center_x = (frame.width - 1) / 2
center_y = (frame.height - 1) / 2
radius_x = max(1.0, center_x - 3)
radius_y = max(1.0, center_y - 2)
distances = [
((cell.x - center_x) / radius_x) ** 2 + ((cell.y - center_y) / radius_y) ** 2
for cell in confetti
]
assert max(distances) > 1.10
assert sum(distance > 0.72 for distance in distances) > len(distances) * 0.4
styles = {cell.style for cell in confetti}
assert "#F9B91C" in styles
assert "#F6C23B" in styles
def test_dot_sphere_front_light_uses_poster_gold_primary() -> None:
frame = build_dot_sphere(width=41, height=19, phase=0.0, state_key="booting")
front_styles = {cell.style for cell in frame.cells if cell.z > 0.05}
assert "#F9B91C" in front_styles
assert "bold #F9B91C" not in front_styles
assert "#FFE600" not in front_styles
def _row_spans(frame: DotSphereFrame) -> dict[int, int]:
spans: dict[int, int] = {}
for y in range(frame.height):
xs = [cell.x for cell in frame.cells if cell.y == y]
spans[y] = max(xs) - min(xs) + 1 if xs else 0
return spans
def _is_braille_cell(glyph: str) -> bool:
return len(glyph) == 1 and 0x2800 < ord(glyph) <= 0x28FF
def _braille_subdot_count(glyph: str) -> int:
assert _is_braille_cell(glyph)
return (ord(glyph) - 0x2800).bit_count()

57
uv.lock
View File

@ -570,6 +570,7 @@ dependencies = [
{ name = "pyyaml" },
{ name = "sqlmodel" },
{ name = "structlog" },
{ name = "textual" },
{ name = "typer" },
{ name = "uvicorn", extra = ["standard"] },
{ name = "watchdog" },
@ -619,6 +620,7 @@ requires-dist = [
{ name = "pyyaml", specifier = ">=6.0" },
{ name = "sqlmodel", specifier = ">=0.0.22" },
{ name = "structlog", specifier = ">=24.0.0" },
{ name = "textual", specifier = ">=8.2.7" },
{ name = "typer", specifier = ">=0.12.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
{ name = "watchdog", specifier = ">=4.0.0" },
@ -1083,6 +1085,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/d0/7e44e8143ac2dae8979ba882cc33d4af7b8da4741fb0361497e69b4a4379/lancedb-0.30.2-cp39-abi3-win_amd64.whl", hash = "sha256:531da53002c1c6fda829afccc8ced3056ef58eb036f09ddb2b94a06877ecc66c", size = 50940681, upload-time = "2026-03-31T23:25:52.35Z" },
]
[[package]]
name = "linkify-it-py"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "uc-micro-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
]
[[package]]
name = "mako"
version = "1.3.12"
@ -1107,6 +1121,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[package.optional-dependencies]
linkify = [
{ name = "linkify-it-py" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
@ -1182,6 +1201,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
]
[[package]]
name = "mdit-py-plugins"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
@ -2122,6 +2153,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" },
]
[[package]]
name = "textual"
version = "8.2.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py", extra = ["linkify"] },
{ name = "mdit-py-plugins" },
{ name = "platformdirs" },
{ name = "pygments" },
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" },
]
[[package]]
name = "tiktoken"
version = "0.12.0"
@ -2259,6 +2307,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
]
[[package]]
name = "uc-micro-py"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"