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:
Pouiiro committed 2026-07-12 17:06:42 +02:00
1 parent 8ae117e604
commit ea9278e746
12 files changed
+407 -79

No files matched your search

+1
View File
@@ -79,3 +79,4 @@ test_tmp/
# Hook test output
__hooks_test__/
.empryo
+67 -78
View File
@@ -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>
BIN
View File
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

+28
View File
@@ -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>
);
}
+2
View File
@@ -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,
+2
View File
@@ -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. */
+137
View File
@@ -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");
});
+1 -1
View File
@@ -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 () => {