Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ Surface hierarchy (strict — do not blur these in code, docs, or UI copy):
## Stack

- Native UI: GPUI with `alacritty_terminal` as the terminal model and render buffer.
- Application core: Rust, SQLite via `rusqlite`, exposed by `crates/runner-app`.
- Application core: Rust, SQLite via `rusqlite`, exposed by `crates/runner-backend`.
- PTY runtime: `portable-pty`.
- Event transport: append-only NDJSON logs watched through `notify`.
- Bundled CLI: `runner`, built from the `cli/` workspace member.

## Project Map

- `crates/runner-native/`: GPUI application, terminal renderer, composer, and terminal fixture corpus.
- `crates/runner-app/`: UI-agnostic application core, including SQLite, session manager, event bus, router, and MCP server.
- `crates/runner-app/`: GPUI application, terminal renderer, and terminal fixture corpus.
- `crates/runner-backend/`: UI-agnostic application core, including SQLite, session manager, event bus, router, and MCP server.
- `cli/`: bundled `runner` CLI used by spawned agents.
- `crates/runner-core/`: shared event-log primitives.
- `design/`: Pencil source files.
Expand All @@ -56,7 +56,7 @@ Surface hierarchy (strict — do not blur these in code, docs, or UI copy):
- Workspace tests: `make test`.
- Everything CI runs: `make verify` (check + test + clippy + fmt-check).

Prefer the smallest check that covers the change. For native UI changes, run the `runner-native` tests plus workspace clippy; for core behavior, run the relevant crate tests.
Prefer the smallest check that covers the change. For native UI changes, run the `runner-app` tests plus workspace clippy; for core behavior, run the relevant crate tests.

## Engineering Conventions

Expand Down
38 changes: 19 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[workspace]
resolver = "2"
members = [
"crates/runner-app",
"crates/runner-backend",
"crates/runner-core",
"crates/runner-native",
"crates/runner-app",
"crates/runner-terminal",
"cli",
]
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ test:
verify: check test clippy fmt-check

run:
cargo run -p runner-native
cargo run -p runner-app
63 changes: 16 additions & 47 deletions crates/runner-app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,58 +1,27 @@
[package]
name = "runner-app"
version = "0.3.17"
description = "UI-agnostic application core for Runner: command bodies, session manager, event bus, router, and MCP server"
edition.workspace = true
authors.workspace = true
publish = false
default-run = "runner-app"

[lib]
name = "runner_app"
[[bin]]
name = "runner-app"
path = "src/main.rs"

[dependencies]
runner-core = { path = "../runner-core" }
log = "0.4"
serde = { workspace = true }
serde_json = { workspace = true }
alacritty_terminal = "0.26"
anyhow = "1"
base64 = "0.22"
rusqlite = { version = "0.32", features = ["bundled", "chrono", "serde_json"] }
# Derived row<->struct mapping for the repo layer (impl 0021). 0.36.x is
# the release whose rusqlite dependency is 0.32, matching the workspace
# rusqlite exactly so the Row types unify.
serde_rusqlite = "0.36"
r2d2 = "0.8"
r2d2_sqlite = "0.25"
chrono = { workspace = true }
thiserror = { workspace = true }
ulid = { workspace = true }
uuid = { workspace = true }
# Regular dep (not dev-only) because the sidecar writer needs
# NamedTempFile::persist for a Windows-safe atomic replace.
tempfile = "3"
# Filesystem watcher for the per-mission event-log tail (C7). The
# `macos_fsevent` backend is the default on macOS; on Linux it falls back to
# inotify, on Windows to ReadDirectoryChangesW.
notify = "6"
# MCP server (impl 0013): Unix socket listener for external clients.
# The `runner-mcp` sidecar bridges stdio ↔ this socket.
rmcp = { version = "1.7", features = ["server"] }
schemars = "1"
tokio = { version = "1", features = ["net", "rt", "io-util", "sync"] }
tokio-util = { version = "0.7", features = ["rt"] }
# Codex MCP config lives in ~/.codex/config.toml; toml_edit preserves
# user-authored comments + formatting on round-trip writes.
toml_edit = "0.25"

# Unix-only deps:
# - libc: FIFO mkfifo + poll() inside session::tmux_runtime.
# - portable-pty: backing crate for session::pty_runtime, the
# in-process replacement for tmux (impl 0011). Windows keeps the
# tmux fallback until a named-pipe equivalent lands.
[target.'cfg(unix)'.dependencies]
libc = "0.2"
portable-pty = "0.9"
futures = "0.3"
gpui = { package = "gpui-ce", version = "0.3" }
runner-backend = { path = "../runner-backend" }
runner-terminal = { path = "../runner-terminal" }
serde.workspace = true
serde_json.workspace = true
tokio = { version = "1", features = ["sync"] }
unicode-segmentation = "1"

[dev-dependencies]
# Used by issue #124's forwarder-contention regression test to hold the
# event-log flock from a second fd; matches the version `runner-core`
# pulls in transitively so the trait impl resolves identically.
fs2 = "0.4"
tempfile = "3"
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# runner-native
# runner-app

Phase 3 walking skeleton for [the gpui-rewrite plan](../../docs/impls/gpui-rewrite/plan.md). It opens Runner's existing direct chats from the same SQLite database as the released app, spawns or resumes them through `runner_app::session::SessionManager`, and renders the manager's PTY byte stream with `alacritty_terminal` on GPUI.
Phase 3 walking skeleton for [the gpui-rewrite plan](../../docs/impls/gpui-rewrite/plan.md). It opens Runner's existing direct chats from the same SQLite database as the released app, spawns or resumes them through `runner_backend::session::SessionManager`, and renders the manager's PTY byte stream with `alacritty_terminal` on GPUI.

## Run

Expand All @@ -17,8 +17,8 @@ GPUI requires the Xcode 26 Metal Toolchain component, already installed on the d
Recorded PTY byte logs in `fixtures/*.ndjson` (header line + base64 data/input/exit events), replayed into a headless `Term` and compared against blessed `*.snapshot.txt` grids:

```sh
cargo test -p runner-native
UPDATE_SNAPSHOTS=1 cargo test -p runner-native
cargo test -p runner-app
UPDATE_SNAPSHOTS=1 cargo test -p runner-app
```

Current corpus: `claude-session` (real interactive TUI boot → prompt → streamed reply → /exit palette), `top-busy` (full-screen redraw churn), `width-torture` (CJK/emoji/ZWJ/box-drawing/SGR glyph classes).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::{bail, Context as _, Result};
use runner_app::{db, event_bus, events, mcp, repo, session, shell_path, windows, AppCore};
use runner_backend::{db, event_bus, events, mcp, repo, session, shell_path, windows, AppCore};

pub const APP_IDENTIFIER: &str = "com.wycstudios.runner";

Expand Down Expand Up @@ -62,7 +62,7 @@ pub fn boot_core(paths: &NativePaths) -> Result<AppCore> {
app_data_dir: paths.app_data_dir.clone(),
sessions,
buses: event_bus::BusRegistry::new(),
routers: runner_app::router::RouterRegistry::new(),
routers: runner_backend::router::RouterRegistry::new(),
mcp: Arc::new(mcp::McpHandle::new()),
windows: window_registry,
events: events::EventChannel::new(),
Expand Down Expand Up @@ -148,7 +148,7 @@ mod tests {
app_data_dir: PathBuf::new(),
sessions: session::SessionManager::new(shell_path::LoginShellEnv::default(), runtime),
buses: event_bus::BusRegistry::new(),
routers: runner_app::router::RouterRegistry::new(),
routers: runner_backend::router::RouterRegistry::new(),
mcp: Arc::new(mcp::McpHandle::new()),
windows: Arc::new(windows::WindowRegistry::new()),
events: events::EventChannel::new(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl NativeRoot {
window: &mut Window,
cx: &mut Context<Self>,
) -> Result<()> {
let _entry = runner_app::ops::session::session_get(&self.core, session_id)?
let _entry = runner_backend::ops::session::session_get(&self.core, session_id)?
.with_context(|| format!("direct chat not found: {session_id}"))?;
let pane_id = layout
.root
Expand Down Expand Up @@ -194,14 +194,14 @@ impl NativeRoot {
return;
};
let keystroke = &event.keystroke;
if runner_native::terminal_ime::terminal_key_route(
if runner_app::terminal_ime::terminal_key_route(
chat.terminal_input.read(cx).is_composing(),
keystroke.modifiers.platform,
keystroke.modifiers.control,
keystroke.modifiers.alt,
keystroke.modifiers.function,
&keystroke.key,
) != runner_native::terminal_ime::TerminalKeyRoute::Raw
) != runner_app::terminal_ime::TerminalKeyRoute::Raw
{
return;
}
Expand Down Expand Up @@ -310,7 +310,7 @@ impl NativeRoot {
};
let mut spawned_id = None;
let result = (|| -> Result<String> {
let spawned = runner_app::ops::session::session_start_direct(
let spawned = runner_backend::ops::session::session_start_direct(
&self.core,
runner_id.to_owned(),
None,
Expand Down Expand Up @@ -371,7 +371,7 @@ impl NativeRoot {
.get(session_id)
.map(|chat| chat.terminal.size())
.unwrap_or_else(|| self.estimated_terminal_size(&layout, pane_id, window));
runner_app::ops::session::session_resume(
runner_backend::ops::session::session_resume(
&self.core,
session_id,
Some(size.0),
Expand Down Expand Up @@ -434,7 +434,7 @@ impl NativeRoot {
.active()
.context("active tab is missing")?
.upsert_input()?;
runner_app::ops::node::node_tab_upsert(&self.core, input)?;
runner_backend::ops::node::node_tab_upsert(&self.core, input)?;
Ok(())
}

Expand Down
96 changes: 5 additions & 91 deletions crates/runner-app/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,91 +1,5 @@
// Runner's UI-agnostic application core (impl 0031 Phase 2).
//
// Everything a frontend needs to run Runner lives here: the SQLite layer,
// the PTY session manager, the per-mission event bus + signal router, the
// MCP server, and the command bodies (`ops`). Frontends — the Tauri app
// today, the native GPUI binary in Phase 3 — are thin adapters: they build
// an `AppCore`, subscribe to its event channel, and delegate their command
// surface to `ops::*`.

pub mod cli_install;
pub mod db;
pub mod error;
pub mod event_bus;
pub mod events;
pub mod mcp;
pub mod model;
pub mod ops;
pub mod repo;
pub mod router;
pub mod session;
pub mod shell_path;
pub mod windows;

use std::path::PathBuf;
use std::sync::Arc;

use events::EventChannel;
use session::manager::CoreSessionEvents;

/// Shared application state. One instance per process, cheap to clone
/// (every field is an `Arc` or small value) — the Tauri layer stores it in
/// `app.manage`, the MCP handler clones it per connection.
#[derive(Clone)]
pub struct AppCore {
pub db: Arc<db::DbPool>,
/// Root of the app's per-user data tree — `$APPDATA/runner/` on real
/// installs, a tempdir in tests. Mission commands resolve event-log paths
/// relative to this via `runner_core::event_log::path`.
pub app_data_dir: PathBuf,
/// Live per-mission session manager. Created at app
/// start, shared across all frontends and the per-session
/// forwarder threads it spawns.
pub sessions: Arc<session::SessionManager>,
/// Live per-mission event-bus watchers. Mounted by `mission_start` once
/// the opening events are durable; unmounted by `mission_stop` and on
/// any rollback path.
pub buses: Arc<event_bus::BusRegistry>,
/// Live per-mission signal routers. Mounted alongside the bus so the
/// router observes the bootstrap `mission_goal` event during initial
/// replay and pushes the launch prompt into the lead's stdin.
pub routers: Arc<router::RouterRegistry>,
/// MCP server lifecycle handle (impl 0013). Unix socket listener
/// that external clients connect to via the `runner-mcp` bridge.
pub mcp: Arc<mcp::McpHandle>,
/// Cross-window coordination map (impl 0018). Tracks which subject
/// (mission / direct chat) each window is looking at + when it
/// was last focused, so exactly one window owns a duplicated subject's
/// PTY.
pub windows: Arc<windows::WindowRegistry>,
/// Broadcast channel every app-observable event flows through. The
/// frontend subscribes and forwards to its own event surface (the
/// Tauri layer re-emits to the webview verbatim).
pub events: EventChannel,
/// The application's user-facing version (the Tauri crate's
/// `CARGO_PKG_VERSION`, which the release bump updates). Advertised by
/// the MCP server's `ServerInfo`.
pub app_version: String,
}

impl AppCore {
/// Session-event sink for spawn/resume/inject call sites. Holds the
/// manager as `Weak` because instances get stored inside the manager's
/// own session state (codex capture context) — a strong ref would cycle.
pub fn session_events(&self) -> CoreSessionEvents {
CoreSessionEvents::new(
Arc::clone(&self.db),
Arc::downgrade(&self.sessions),
Arc::clone(&self.windows),
self.events.clone(),
)
}

/// Broadcast the current window→subject map. Called after every window
/// registry mutation so all windows converge on a consistent picture of
/// who owns what. Broadcast, not targeted: each window filters by its
/// own subject (spec decision 5).
pub fn broadcast_focus_map(&self) {
self.events
.emit("window_focus_map", &self.windows.snapshot());
}
}
pub mod bootstrap;
pub mod pane_layout;
pub mod terminal_ime;
pub mod terminal_resize;
pub mod text_util;
Loading
Loading