mirror of
https://github.com/proxysoul/Empryo.git
synced 2026-11-07 06:25:57 -10:00
feat: reface repo as Empryo + one-time TUI announcement
- README: rebrand to Empryo (previously SoulForge) — copied Empryo logo mark, concise copy, install, benchmarks vs Pi, changelog link; SoulForge stays downloadable with bug/critical fixes while development moves to Empryo - TUI: one-time "SoulForge is now Empryo" popup (Close / Don't-show-again) reusing the update-popup chrome — green Empryo wordmark, gratitude note, macOS/Linux/Windows; persists empryoAnnouncementDismissed - tests: headless-render e2e drive the popup keyboard + onClose contract + store wiring - fix: refresh stale structural-edit ast-grep assertion - chore: gitignore runtime .empryo dir
This commit is contained in:
12 files changed
+407
-79
No files matched your search
@@ -79,3 +79,4 @@ test_tmp/
|
||||
|
||||
# Hook test output
|
||||
__hooks_test__/
|
||||
.empryo
|
||||
@@ -1,122 +1,111 @@
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="assets/SOULFORGE_LOGO.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="assets/SOULFORGE_LOGO_LIGHT.png" />
|
||||
<img alt="SoulForge" src="assets/SOULFORGE_LOGO.png" width="560" />
|
||||
</picture>
|
||||
<img src="assets/empryo-mark.png" width="150" alt="Empryo" />
|
||||
|
||||
<p><strong>The AI coding agent that edits symbols, not strings.</strong></p>
|
||||
# Empryo
|
||||
|
||||
<p>
|
||||
<a href="https://www.npmjs.com/package/@proxysoul/soulforge"><img alt="npm" src="https://img.shields.io/npm/v/@proxysoul/soulforge?label=npm&color=7844f0&style=flat-square" /></a>
|
||||
<a href="https://github.com/ProxySoul/soulforge/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/ProxySoul/soulforge/ci.yml?label=ci&style=flat-square" /></a>
|
||||
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-BSL%201.1-blue.svg?style=flat-square" /></a>
|
||||
<a href="https://soulforge.proxysoul.com"><img alt="Docs" src="https://img.shields.io/badge/docs-soulforge.proxysoul.com-555.svg?style=flat-square" /></a>
|
||||
<a href="https://discord.gg/fX4H7GYSMJ"><img alt="Discord" src="https://img.shields.io/badge/discord-join-5865F2.svg?style=flat-square&logo=discord&logoColor=white" /></a>
|
||||
</p>
|
||||
<sub>previously **SoulForge**</sub>
|
||||
|
||||
**Graph-powered code intelligence — multi-agent coding with codebase-aware AI.**
|
||||
|
||||
[Website](https://empryo.com) · [Download](https://empryo.com/download) · [Changelog](https://empryo.com/changelog) · [Discussions](https://github.com/proxysoul/soulforge/discussions) · [Discord](https://discord.gg/fX4H7GYSMJ)
|
||||
|
||||
<img alt="Empryo terminal UI" src="assets/TUI-1.png" width="880" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**SoulForge is now Empryo** — the same symbol-level, graph-powered agent, rebuilt with a desktop app, a faster engine, and a composable core. Development continues on Empryo; this repository is its home for issues and discussions.
|
||||
|
||||
## What is Empryo?
|
||||
|
||||
Empryo understands your codebase as a **graph**, not a pile of text. It parses your repo with tree-sitter, ranks symbols with PageRank and git co-change, and edits code at the **symbol level** — so it reads what matters and changes exactly what it means to.
|
||||
|
||||
It runs in a fast terminal UI, a native desktop app, and a scriptable headless mode. **Free to use** — bring your own provider key, or run fully local with Ollama. Your code never leaves your machine.
|
||||
|
||||
<div align="center">
|
||||
<img alt="Empryo desktop app" src="assets/desktop-2.png" width="880" />
|
||||
</div>
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Live code graph** | tree-sitter parse, PageRank + git co-change, blast-radius tags, FTS5 + trigram search |
|
||||
| **Symbol-level editing** | AST edits via ts-morph (65+ ops, atomic batches) + structural edits across 30+ languages |
|
||||
| **Multi-agent** | parallel explore/edit agents with a shared I/O cache |
|
||||
| **Task router** | route each slot to a different model per tab — cheap to explore, strong to code |
|
||||
| **Time machine** | every prompt is a checkpoint with a git tag; rewind and redo conversation *and* files |
|
||||
| **22 providers** | Anthropic, OpenAI, Google, Groq, DeepSeek, Bedrock, Ollama, LM Studio … + any OpenAI-compatible |
|
||||
| **Desktop app** | native window, embedded browser, and the full agent |
|
||||
| **LSP + MCP** | 576+ language servers via Mason, any MCP server, 13 lifecycle hooks |
|
||||
|
||||
Runs on **macOS, Linux, and Windows**.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
brew tap proxysoul/tap && brew install soulforge
|
||||
# macOS / Linux
|
||||
curl -fsSL https://empryo.com/install.sh | bash
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://empryo.com/install.ps1 | iex
|
||||
```
|
||||
|
||||
```bash
|
||||
# alternatives
|
||||
bun install -g @proxysoul/soulforge
|
||||
# or download a prebuilt binary from https://github.com/ProxySoul/soulforge/releases/latest
|
||||
brew install proxysoul/tap/empryo
|
||||
winget install ProxySoul.Empryo
|
||||
```
|
||||
|
||||
macOS, Linux, and Windows 10 1809+ / Windows 11 (x64). On Windows, grab `soulforge-<version>-windows-x64.zip` (portable) or `soulforge-setup-<version>-x64.exe` (setup wizard) from the [latest release](https://github.com/proxysoul/soulforge/releases/latest). ARM64 not yet supported (tracked upstream).
|
||||
|
||||
## Quick start
|
||||
Desktop app and prebuilt binaries: [empryo.com/download](https://empryo.com/download).
|
||||
|
||||
```bash
|
||||
soulforge --set-key anthropic sk-ant-...
|
||||
empryo --set-key anthropic sk-ant-... # or run local with Ollama, no key
|
||||
cd your-project
|
||||
soulforge
|
||||
empryo
|
||||
```
|
||||
|
||||
Other providers and OpenAI-compatible endpoints: [docs/providers](https://soulforge.proxysoul.com/providers).
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Same model (Claude Opus 4.6), same codebase, same prompt.
|
||||
Same models, same repo, same tasks — Empryo's graph does more with less.
|
||||
|
||||
**Bug fix**
|
||||
| | Empryo | Pi |
|
||||
|---|:---:|:---:|
|
||||
| Bugs fixed | **8 / 9** | 7 / 9 |
|
||||
| Cost | **$1.13** | $1.58 |
|
||||
| Wall-clock | **4m 16s** | 10m 0s |
|
||||
| Input tokens | **1.09M** | 6.21M |
|
||||
|
||||
| | SoulForge | OpenCode |
|
||||
|---|---|---|
|
||||
| Time | **6m 22s** | 11m 18s |
|
||||
| Cost | **$1.70** | $3.52 |
|
||||
| Result | Correct | Correct |
|
||||
Empryo navigates by symbols and call hierarchies instead of grep-and-read, so it takes fewer steps and burns fewer tokens. Full methodology and transcripts: [empryo.com/benchmarks](https://empryo.com/benchmarks) · reproduce it at [proxysoul/pi-vs-empryo-bench](https://github.com/proxysoul/pi-vs-empryo-bench).
|
||||
|
||||
**Audit task** (*"verify cost reporting is wired correctly"*)
|
||||
## SoulForge
|
||||
|
||||
| | SoulForge | OpenCode |
|
||||
|---|---|---|
|
||||
| Time | **2m 00s** | 5m 56s |
|
||||
| Cost | **$0.84** | $2.61 |
|
||||
| Accuracy | **7/7 (100%)** | 4/7 (57%) |
|
||||
| False alarms | **0** | 3 |
|
||||
| Wrong claims | **0** | 1 |
|
||||
SoulForge remains available to download and install, and we'll keep fixing bugs and critical issues. New features and active development move to Empryo.
|
||||
|
||||
> Same bug. Same model. Same repo. Half the time. Half the cost.
|
||||
```bash
|
||||
brew tap proxysoul/tap && brew install soulforge
|
||||
# or
|
||||
bun install -g @proxysoul/soulforge
|
||||
```
|
||||
|
||||
<sub>Sources: [recording 1](https://x.com/BniWael/status/2040172009666015641/video/1) · [recording 2](https://x.com/BniWael/status/2042364421373121018/video/1) · [recording 3](https://x.com/BniWael/status/2044826445382373759/video/1)</sub>
|
||||
## This repository
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | What it does |
|
||||
|---|---|
|
||||
| **AST editing** | TS/JS edits via ts-morph, 65+ ops, atomic batches. [docs](https://soulforge.proxysoul.com/tools/ast-edit) |
|
||||
| **Live Soul Map** | SQLite graph, PageRank + git co-change, blast-radius tags. [docs](https://soulforge.proxysoul.com/concepts/repo-map) |
|
||||
| **LSP + Mason** | 576+ servers installable from the TUI |
|
||||
| **33 languages** | symbol-level reads, not file dumps |
|
||||
| **Compound tools** | `rename_symbol`, `move_symbol`, `refactor`, `project` (23 toolchains) |
|
||||
| **Task router** | route each slot (spark / ember / compact / verify / web / semantic) to a different model per tab. Haiku for exploration, Sonnet for code, Flash for compaction. `/router`. [docs](https://soulforge.proxysoul.com/recipes/task-router) |
|
||||
| **V2 compaction** | usually 0 LLM tokens. [docs](https://soulforge.proxysoul.com/context/compaction) |
|
||||
| **Parallel agents** | Spark + Ember with shared I/O cache |
|
||||
| **Embedded Neovim** | real nvim in a PTY, your config |
|
||||
| **5 tabs** | per-tab model, session, checkpoints, file claims |
|
||||
| **Time machine** | every prompt is a checkpoint with a git tag. `Ctrl+B` / `Ctrl+F` rewinds and redoes both conversation and files on disk. `/checkpoint undo <N>`, `/checkpoint save`, per-tab |
|
||||
| **Sessions** | auto-saved JSONL, crash-resilient, resumable by short-id prefix. Export to markdown / JSON / clipboard. `Ctrl+P` browser, multi-tab. [docs](https://soulforge.proxysoul.com/tools/sessions) |
|
||||
| **Memory** | cross-session SQLite store of prefs, decisions, gotchas, context. Auto-recalled per turn from prompt + edited files. Project + global scopes, browser at `/memory`. [docs](https://soulforge.proxysoul.com/tools/memory) |
|
||||
| **21 providers** | Anthropic, OpenAI, Google, Groq, DeepSeek, Bedrock, Ollama, LM Studio, ... + any OpenAI-compatible |
|
||||
| **Cost tracking** | per-model + per-subagent USD, cache-aware |
|
||||
| **MCP + hooks** | any MCP server, 13 events, drop-in `.claude/settings.json` |
|
||||
| **Headless mode** | run from CI, scripts, pipelines. JSON / event stream, resumable sessions. [docs](https://soulforge.proxysoul.com/recipes/headless) |
|
||||
| **Hearth** *(exp)* | remote control via Telegram or Discord, your host only |
|
||||
| **36 themes** | hot-reloaded JSON, Kitty inline images |
|
||||
|
||||
## License
|
||||
|
||||
[BSL 1.1](LICENSE). Free for personal and internal use. Commercial use: [commercial license](COMMERCIAL_LICENSE.md). Converts to Apache 2.0 on March 15, 2030.
|
||||
- **[Issues](https://github.com/proxysoul/soulforge/issues)** and **[Discussions](https://github.com/proxysoul/soulforge/discussions)** — the home for Empryo bug reports, questions, and ideas.
|
||||
- The SoulForge source stays archived here under its existing license (see [`LICENSE`](LICENSE)).
|
||||
|
||||
## Sponsors
|
||||
|
||||
<div align="center">
|
||||
|
||||
<sub><b>Backed by</b></sub>
|
||||
|
||||
<a href="https://llmgateway.io/dashboard?ref=6tjJR2H3X4E9RmVQiQwK" title="LLM Gateway">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="assets/llmg-white.svg" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="assets/llmg-dark.svg" />
|
||||
<img alt="LLM Gateway" src="assets/llmg-dark.svg" height="56" />
|
||||
<img alt="LLM Gateway" src="assets/llmg-dark.svg" height="52" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<p><sub><i>One API, 200+ models, up to 30% off frontier. Wired into SoulForge as the <code>llmgateway</code> provider.</i></sub></p>
|
||||
<sub>One API, 200+ models, up to 30% off frontier. Wired in as the <code>llmgateway</code> provider.</sub>
|
||||
|
||||
<a href="https://llmgateway.io/dashboard?ref=6tjJR2H3X4E9RmVQiQwK"><img src="https://img.shields.io/badge/Get_an_LLM_Gateway_key-7C3AED.svg?style=for-the-badge&labelColor=0a0818" alt="Get an LLM Gateway key" /></a>
|
||||
|
||||
<br/>
|
||||
|
||||
<sub><a href="https://github.com/sponsors/proxysoul">Sponsor on GitHub</a> (monthly or one-time) · <a href="https://paypal.me/waeru">PayPal</a> (one-time) · <a href="BACKERS.md">All backers</a></sub>
|
||||
<sub><a href="https://github.com/sponsors/proxysoul">Sponsor</a> · <a href="https://paypal.me/waeru">PayPal</a> · <a href="BACKERS.md">All backers</a></sub>
|
||||
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -68,6 +68,7 @@ import { SimpleModalLayer } from "./ModalLayer.js";
|
||||
import { CommandPalette } from "./modals/CommandPalette.js";
|
||||
import { CommandPicker } from "./modals/CommandPicker.js";
|
||||
import { DiagnosePopup } from "./modals/DiagnosePopup.js";
|
||||
import { EmpryoAnnouncement } from "./modals/EmpryoAnnouncement.js";
|
||||
import { FirstRunWizard } from "./modals/FirstRunWizard.js";
|
||||
import { GitCommitModal } from "./modals/GitCommitModal.js";
|
||||
import { GitMenu } from "./modals/GitMenu.js";
|
||||
@@ -572,6 +573,7 @@ export function App({
|
||||
const modalMCPSettings = useUIStore((s) => s.modals.mcpSettings);
|
||||
const modalHearthSettings = useUIStore((s) => s.modals.hearthSettings);
|
||||
const modalFirstRunWizard = useUIStore((s) => s.modals.firstRunWizard);
|
||||
const modalEmpryoAnnouncement = useUIStore((s) => s.modals.empryoAnnouncement);
|
||||
const modalUpdateModal = useUIStore((s) => s.modals.updateModal);
|
||||
const modalTabNamePopup = useUIStore((s) => s.modals.tabNamePopup);
|
||||
const modalMemoryBrowser = useUIStore((s) => s.modals.memoryBrowser);
|
||||
@@ -624,6 +626,24 @@ export function App({
|
||||
}
|
||||
}, [config.onboardingComplete, forceWizard, resumeSessionId]);
|
||||
|
||||
// One-time "SoulForge is now Empryo" announcement. Shows once per install
|
||||
// (until dismissed), sequenced after the setup/first-run modals so it never
|
||||
// clobbers them. Persisted via `empryoAnnouncementDismissed` in global config.
|
||||
const announcementShown = useRef(false);
|
||||
useEffect(() => {
|
||||
if (config.empryoAnnouncementDismissed || announcementShown.current) return;
|
||||
if (getMissingRequired().length > 0) return;
|
||||
const ui = useUIStore.getState();
|
||||
if (ui.modals.setup || ui.modals.firstRunWizard) return; // wait for them to close
|
||||
const timer = setTimeout(() => {
|
||||
const u = useUIStore.getState();
|
||||
if (u.modals.setup || u.modals.firstRunWizard || selectIsAnyModalOpen(u)) return;
|
||||
announcementShown.current = true;
|
||||
u.openModal("empryoAnnouncement");
|
||||
}, 600);
|
||||
return () => clearTimeout(timer);
|
||||
}, [config.empryoAnnouncementDismissed]);
|
||||
|
||||
const cwd = getCwd();
|
||||
|
||||
const saveToScope = useCallback(
|
||||
@@ -2084,6 +2104,14 @@ export function App({
|
||||
|
||||
<UpdateModal visible={modalUpdateModal} onClose={getCloser("updateModal")} />
|
||||
|
||||
<EmpryoAnnouncement
|
||||
visible={modalEmpryoAnnouncement}
|
||||
onClose={(dontShowAgain) => {
|
||||
useUIStore.getState().closeModal("empryoAnnouncement");
|
||||
if (dontShowAgain) saveToScope({ empryoAnnouncementDismissed: true }, "global");
|
||||
}}
|
||||
/>
|
||||
|
||||
<TabNamePopup
|
||||
visible={modalTabNamePopup}
|
||||
placeholder={`TAB-${String(tabMgr.tabCount + 1)}`}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* EmpryoAnnouncement — one-time "SoulForge is now Empryo" notice.
|
||||
*
|
||||
* Reuses the update-popup chrome (PremiumPopup) with two focusable buttons:
|
||||
* • Close — dismiss for this launch only.
|
||||
* • Don't show again — persist so it never reappears.
|
||||
*
|
||||
* The Empryo wordmark (half-block bake, copied from the Empryo TUI's
|
||||
* `@empryo/base/utils/splash`) is rendered in Empryo green as the hero.
|
||||
*
|
||||
* Wired in App.tsx: opens once on startup when
|
||||
* `config.empryoAnnouncementDismissed` is not set, and "Don't show again"
|
||||
* persists that flag to the global config.
|
||||
*/
|
||||
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { icon } from "../../core/icons.js";
|
||||
import { useTheme } from "../../core/theme/index.js";
|
||||
import { openPath } from "../../core/utils/open-path.js";
|
||||
import { Button, PremiumPopup, Section, VSpacer } from "../ui/index.js";
|
||||
|
||||
const BOLD = TextAttributes.BOLD;
|
||||
|
||||
const EMPRYO_URL = "https://empryo.com";
|
||||
/** Empryo brand green — the seedling/growth mark. */
|
||||
const EMPRYO_GREEN = "#4ade80";
|
||||
const EMPRYO_GREEN_DIM = "#2f9e57";
|
||||
|
||||
/** "empryo" half-block wordmark, copied from the Empryo TUI splash. */
|
||||
const WORDMARK = [
|
||||
"▄▀▀▄ ▄ ▄ █▀▀▄ █▀▀▄ █ █ ▄▀▀▄",
|
||||
"█▄▄▀ █▄█▄ █▄▄▀ █▀▀ ▀▄▄█ █ █",
|
||||
"▀▄▄▄ █ █ █ ▀ ▄▄▄▀ ▀▄▄▀",
|
||||
];
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
/** Called on close. `dontShowAgain` = user chose to never see it again. */
|
||||
onClose: (dontShowAgain: boolean) => void;
|
||||
}
|
||||
|
||||
/** Focus index → the two action buttons. */
|
||||
const CLOSE = 0;
|
||||
const DISMISS = 1;
|
||||
|
||||
export function EmpryoAnnouncement({ visible, onClose }: Props) {
|
||||
const t = useTheme();
|
||||
const { width: termCols, height: termRows } = useTerminalDimensions();
|
||||
const [focus, setFocus] = useState<number>(CLOSE);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) setFocus(CLOSE);
|
||||
}, [visible]);
|
||||
|
||||
const pw = Math.min(76, Math.max(58, Math.floor(termCols * 0.78)));
|
||||
const popupH = Math.min(30, Math.max(22, termRows - 4));
|
||||
const bg = t.bgPopup;
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (!visible) return;
|
||||
if (evt.name === "escape" || evt.name === "q") {
|
||||
onClose(false);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Direct shortcuts mirror the update popup: single-key actions.
|
||||
if (evt.name === "d") {
|
||||
onClose(true);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (evt.name === "g") {
|
||||
openPath(EMPRYO_URL);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (evt.name === "left" || evt.name === "h") {
|
||||
setFocus(CLOSE);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (evt.name === "right" || evt.name === "l") {
|
||||
setFocus(DISMISS);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (evt.name === "tab") {
|
||||
setFocus((f) => (f === CLOSE ? DISMISS : CLOSE));
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (evt.name === "return" || evt.name === "space") {
|
||||
onClose(focus === DISMISS);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
evt.preventDefault();
|
||||
});
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<PremiumPopup
|
||||
visible={visible}
|
||||
width={pw}
|
||||
height={popupH}
|
||||
borderColor={EMPRYO_GREEN}
|
||||
title="SoulForge is now Empryo"
|
||||
titleIcon="sparkle"
|
||||
status="online"
|
||||
footerHints={[
|
||||
{ key: "←→", label: "select" },
|
||||
{ key: "↵", label: "confirm" },
|
||||
{ key: "Esc", label: "close" },
|
||||
]}
|
||||
>
|
||||
<Section>
|
||||
{WORDMARK.map((line, i) => (
|
||||
<text
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static 3-line wordmark
|
||||
key={`wm-${i}`}
|
||||
bg={bg}
|
||||
fg={i === WORDMARK.length - 1 ? EMPRYO_GREEN_DIM : EMPRYO_GREEN}
|
||||
attributes={BOLD}
|
||||
>
|
||||
{line}
|
||||
</text>
|
||||
))}
|
||||
|
||||
<VSpacer rows={1} bg={bg} />
|
||||
|
||||
<text bg={bg} fg={t.textSecondary}>
|
||||
Thank you for trusting SoulForge.
|
||||
</text>
|
||||
<text bg={bg}>
|
||||
<span fg={t.textPrimary}>What began as a simple idea is now </span>
|
||||
<span fg={EMPRYO_GREEN} attributes={BOLD}>
|
||||
Empryo
|
||||
</span>
|
||||
<span fg={t.textPrimary}> —</span>
|
||||
</text>
|
||||
<text bg={bg} fg={t.textPrimary}>
|
||||
symbol-aware, multi-agent, and much faster.
|
||||
</text>
|
||||
|
||||
<VSpacer rows={1} bg={bg} />
|
||||
|
||||
<text bg={bg}>
|
||||
<span fg={t.textMuted}>{icon("globe")} </span>
|
||||
<span fg={EMPRYO_GREEN} attributes={BOLD}>
|
||||
empryo.com
|
||||
</span>
|
||||
<span fg={t.textFaint}>{" · macOS · Linux · Windows"}</span>
|
||||
</text>
|
||||
<text bg={bg} fg={t.textFaint}>
|
||||
SoulForge still works — Empryo is where it grows.
|
||||
</text>
|
||||
|
||||
<box flexDirection="row" backgroundColor={bg}>
|
||||
<Button label="Close" focused={focus === CLOSE} bg={bg} />
|
||||
<text bg={bg}> </text>
|
||||
<Button label="Don't show again" variant="ghost" focused={focus === DISMISS} bg={bg} />
|
||||
</box>
|
||||
</Section>
|
||||
</PremiumPopup>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export type ModalName =
|
||||
| "firstRunWizard"
|
||||
| "floatingTerminal"
|
||||
| "updateModal"
|
||||
| "empryoAnnouncement"
|
||||
| "mcpSettings"
|
||||
| "hearthSettings"
|
||||
| "tabNamePopup"
|
||||
@@ -67,6 +68,7 @@ const INITIAL_MODALS: Modals = {
|
||||
firstRunWizard: false,
|
||||
floatingTerminal: false,
|
||||
updateModal: false,
|
||||
empryoAnnouncement: false,
|
||||
mcpSettings: false,
|
||||
hearthSettings: false,
|
||||
tabNamePopup: false,
|
||||
|
||||
@@ -397,6 +397,8 @@ export interface AppConfig {
|
||||
keyPriority?: "env" | "app";
|
||||
/** Whether the first-run onboarding wizard has been completed. */
|
||||
onboardingComplete?: boolean;
|
||||
/** Whether the "SoulForge is now Empryo" announcement has been dismissed for good. */
|
||||
empryoAnnouncementDismissed?: boolean;
|
||||
/** MCP servers to connect to. Each entry spawns a subprocess (stdio) or connects via HTTP+SSE. */
|
||||
mcpServers?: MCPServerConfig[];
|
||||
/** Auto-retry on stream stalls. Default: false (disabled). Toggle via /watchdog. */
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* e2e coverage for the "SoulForge is now Empryo" announcement popup.
|
||||
*
|
||||
* Drives the real OpenTUI React reconciler through the headless test renderer:
|
||||
* mounts the modal, asserts what's painted, presses keys, and checks the
|
||||
* onClose contract (dontShowAgain flag) that App.tsx relies on to persist
|
||||
* `empryoAnnouncementDismissed`.
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import type { KeyInput } from "@opentui/core/testing";
|
||||
import { testRender } from "@opentui/react/test-utils";
|
||||
import { EmpryoAnnouncement } from "../src/components/modals/EmpryoAnnouncement.js";
|
||||
import { selectIsAnyModalOpen, useUIStore } from "../src/stores/ui.js";
|
||||
|
||||
const OPTS = { width: 90, height: 30 } as const;
|
||||
|
||||
async function mount() {
|
||||
const calls: boolean[] = [];
|
||||
const setup = await testRender(
|
||||
<EmpryoAnnouncement visible onClose={(d) => calls.push(d)} />,
|
||||
OPTS,
|
||||
);
|
||||
await setup.flush();
|
||||
await setup.renderOnce();
|
||||
// Press a key and let both the terminal parser and React's commit settle
|
||||
// before returning (state updates from the keypress must land).
|
||||
const press = async (key: KeyInput) => {
|
||||
await setup.mockInput.pressKey(key);
|
||||
await setup.flush();
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
await setup.renderOnce();
|
||||
};
|
||||
return { ...setup, calls, press };
|
||||
}
|
||||
|
||||
test("announcement renders the gratitude, platforms, repo notice, and both buttons", async () => {
|
||||
const { captureCharFrame } = await mount();
|
||||
const frame = captureCharFrame();
|
||||
expect(frame).toContain("SoulForge is now Empryo");
|
||||
// Gratitude message.
|
||||
expect(frame).toContain("Thank you for trusting SoulForge");
|
||||
// Brand + platforms + link.
|
||||
expect(frame).toContain("empryo.com");
|
||||
expect(frame).toContain("macOS · Linux · Windows");
|
||||
// Closing line: SoulForge lives on, Empryo is the future.
|
||||
expect(frame).toContain("SoulForge still works");
|
||||
// Two actionable buttons are painted.
|
||||
expect(frame).toContain("Close");
|
||||
expect(frame).toContain("Don't show again");
|
||||
});
|
||||
|
||||
test("Enter on the default focus (Close) does not persist", async () => {
|
||||
const { press, calls } = await mount();
|
||||
await press("RETURN");
|
||||
expect(calls).toEqual([false]);
|
||||
});
|
||||
|
||||
test("'d' shortcut persists (dontShowAgain = true)", async () => {
|
||||
const { press, calls } = await mount();
|
||||
await press("d");
|
||||
expect(calls).toEqual([true]);
|
||||
});
|
||||
|
||||
test("Tab to the second button, then Enter persists (dontShowAgain = true)", async () => {
|
||||
const { press, calls } = await mount();
|
||||
await press("TAB"); // focus → "Don't show again"
|
||||
await press("RETURN");
|
||||
expect(calls).toEqual([true]);
|
||||
});
|
||||
|
||||
test("Tab twice returns focus to Close → Enter does not persist", async () => {
|
||||
const { press, calls } = await mount();
|
||||
await press("TAB"); // → Don't show again
|
||||
await press("TAB"); // → back to Close
|
||||
await press("RETURN");
|
||||
expect(calls).toEqual([false]);
|
||||
});
|
||||
|
||||
test("'q' closes without persisting", async () => {
|
||||
const { press, calls } = await mount();
|
||||
await press("q");
|
||||
expect(calls).toEqual([false]);
|
||||
});
|
||||
|
||||
test("hidden when not visible", async () => {
|
||||
const setup = await testRender(
|
||||
<EmpryoAnnouncement visible={false} onClose={() => {}} />,
|
||||
OPTS,
|
||||
);
|
||||
await setup.flush();
|
||||
await setup.renderOnce();
|
||||
expect(setup.captureCharFrame()).not.toContain("SoulForge is now Empryo");
|
||||
});
|
||||
|
||||
// Integration: exercise the exact wiring App.tsx uses — the UI store drives
|
||||
// `visible`, a keypress calls onClose, and onClose closes the modal via the store.
|
||||
test("store-driven open → render → keypress → close (App wiring path)", async () => {
|
||||
const persisted: boolean[] = [];
|
||||
|
||||
function Harness() {
|
||||
const visible = useUIStore((s) => s.modals.empryoAnnouncement);
|
||||
return (
|
||||
<EmpryoAnnouncement
|
||||
visible={visible}
|
||||
onClose={(dontShowAgain) => {
|
||||
useUIStore.getState().closeModal("empryoAnnouncement");
|
||||
persisted.push(dontShowAgain);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const setup = await testRender(<Harness />, OPTS);
|
||||
await setup.flush();
|
||||
await setup.renderOnce();
|
||||
|
||||
// Store closed → nothing painted.
|
||||
expect(setup.captureCharFrame()).not.toContain("SoulForge is now Empryo");
|
||||
expect(selectIsAnyModalOpen(useUIStore.getState())).toBe(false);
|
||||
|
||||
// App opens it the same way the startup effect does.
|
||||
useUIStore.getState().openModal("empryoAnnouncement");
|
||||
await setup.flush();
|
||||
await setup.renderOnce();
|
||||
expect(setup.captureCharFrame()).toContain("SoulForge is now Empryo");
|
||||
|
||||
// "Don't show again" → onClose(true) → store closes the modal.
|
||||
await setup.mockInput.pressKey("d");
|
||||
await setup.flush();
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
await setup.renderOnce();
|
||||
|
||||
expect(persisted).toEqual([true]);
|
||||
expect(useUIStore.getState().modals.empryoAnnouncement).toBe(false);
|
||||
expect(setup.captureCharFrame()).not.toContain("SoulForge is now Empryo");
|
||||
});
|
||||
@@ -101,7 +101,7 @@ describe("structural_edit", () => {
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
expect(r.error).toBe("missing-binary");
|
||||
expect(r.output).toContain("ast-grep not found");
|
||||
expect(r.output).toContain("ast-grep is not yet available");
|
||||
});
|
||||
|
||||
test("rewrites a Python function name (when ast-grep present)", async () => {
|
||||
|
||||
Reference in new issue
Block a user