feat(yahweh-launcher): F3 — extracción del shell standard de explorers

Iter 19. Patrón con 4 consumers idénticos: cada main() repetía el mismo
~20 líneas de boot (Application::new + Theme::install_default +
cx.open_window + WindowOptions + cx.activate). Sólo varían título,
tamaño y root factory.

crates/modules/ui_engine/libs/launcher/:
- pub fn launch_app(title, size, root_factory) → 1-line boot.
- pub fn launch_app_with(config, root_factory) → variante con config
  armado afuera (env-var driven, etc).
- pub struct AppLaunchConfig::new(title, size).
- 2 tests cubren normalización del config.

Migración 4 consumers (nakui/nouser/minga/brahman-broker explorer):
- main() pasa de ~20 líneas a 1: launch_app(...).
- Imports gpui podados (no más App/Application/Bounds/WindowOpts/etc).
- Cada uno agrega dep yahweh-launcher.

Naming: yahweh-shell ya existe (bootstrap heavyweight con file/db/text
viewers en crates/apps/). Helper liviano queda como yahweh-launcher.

Ahorro ~75 líneas de boot hardcoded. Cambios de window/theme boot
ahora en un solo lugar.

2/2 tests launcher; 4 consumer suites intactas, todo verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-10 15:19:17 +00:00
parent 4b8bd389c9
commit 37e40073ef
13 changed files with 207 additions and 85 deletions
+38
View File
@@ -6,6 +6,44 @@ ratio/diff ver `git show <sha>`.
## 2026-05-10
### feat(yahweh-launcher): F3 — extracción del shell standard de explorers
Iter 19. Patrón con 4 consumers idénticos (nakui-explorer,
nouser-explorer, minga-explorer, brahman-broker-explorer) declaraban
~20 líneas de boot:
`Application::new + Theme::install_default + cx.open_window
+ WindowOptions{ window_bounds, titlebar } + cx.activate(true)`.
Todo idéntico salvo título, tamaño, y root entity factory.
Crate nuevo `crates/modules/ui_engine/libs/launcher/`
(`yahweh-launcher`):
- **Deps**: `gpui`, `yahweh-theme`. Sin más.
- **`pub fn launch_app(title, size, root_factory)`**: 1-line boot.
- **`pub fn launch_app_with(config, root_factory)`**: variante con
`AppLaunchConfig` armado afuera, para casos donde título/tamaño se
computan condicionalmente (env var, etc).
- **`AppLaunchConfig::new(title, size)`**: builder normalizador.
- 2 tests `#[test]` cubren la normalización de config (no se testea
`launch_app` per se porque bloquea el thread main hasta que la
ventana se cierra).
Migración 4 consumers:
- `main()` pasa de 20 líneas a 1: `launch_app("Title", (W, H), Explorer::new)`.
- Imports de gpui se podan: ya no necesitan `App, Application,
Bounds, WindowBounds, WindowOptions` ni `SharedString` ni `prelude::*`.
- Cada consumer agrega dep `yahweh-launcher` (path local).
Naming: el slot natural era `yahweh-shell`, pero ya existe un crate
`yahweh-shell` en `crates/apps/` (bootstrap heavyweight con file
explorer + DB explorer + viewers). El helper es liviano y específico
al patrón de launch, así que `yahweh-launcher` evita confusión.
Total ahorro: ~75 líneas hardcoded de boilerplate UI a 4 líneas en
total. Cambios de boot (window opts, theme, etc) ahora viven en un
solo lugar.
Tests stack: 2 nuevos en launcher; suites de los 4 consumers
intactas. Todo verde.
### chore(.gitignore): excluir .claude/ (state local de Claude Code)
Iter 18. Side cleanup tras debugging: `.claude/` aparecía en
`git status` cada sesión (contenía `scheduled_tasks.lock` y
Generated
+12
View File
@@ -1242,6 +1242,7 @@ dependencies = [
"brahman-handshake",
"brahman-sidecar",
"gpui",
"yahweh-launcher",
"yahweh-theme",
"yahweh-widget-app-header",
"yahweh-widget-banner",
@@ -6215,6 +6216,7 @@ version = "0.1.0"
dependencies = [
"gpui",
"minga-store",
"yahweh-launcher",
"yahweh-theme",
"yahweh-widget-app-header",
"yahweh-widget-banner",
@@ -6484,6 +6486,7 @@ dependencies = [
"serde_json",
"tempfile",
"uuid",
"yahweh-launcher",
"yahweh-meta-runtime",
"yahweh-theme",
"yahweh-widget-app-header",
@@ -6882,6 +6885,7 @@ dependencies = [
"brahman-sidecar",
"gpui",
"nouser-card",
"yahweh-launcher",
"yahweh-theme",
"yahweh-widget-app-header",
"yahweh-widget-banner",
@@ -12944,6 +12948,14 @@ dependencies = [
"yahweh-theme",
]
[[package]]
name = "yahweh-launcher"
version = "0.1.0"
dependencies = [
"gpui",
"yahweh-theme",
]
[[package]]
name = "yahweh-meta-runtime"
version = "0.1.0"
+1
View File
@@ -51,6 +51,7 @@ members = [
# ============================================================
"crates/modules/ui_engine/libs/core",
"crates/modules/ui_engine/libs/theme",
"crates/modules/ui_engine/libs/launcher",
"crates/modules/ui_engine/libs/bus",
"crates/modules/ui_engine/libs/meta-schema",
"crates/modules/ui_engine/libs/meta-runtime",
@@ -9,6 +9,7 @@ description = "Probe GUI del broker brahman: conecta cada N segundos vía await_
brahman-handshake = { path = "../../core/brahman-handshake" }
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" }
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
@@ -29,35 +29,19 @@ use std::time::{Duration, Instant};
use brahman_handshake::transport;
use brahman_sidecar::{await_provider_blocking, build_consumer_card, ConsumerError};
use gpui::{
div, prelude::*, px, App, Application, Bounds, Context, IntoElement, Render, SharedString,
Window, WindowBounds, WindowOptions,
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
};
use yahweh_launcher::launch_app;
use yahweh_theme::Theme;
use yahweh_widget_app_header::app_header;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_stat_card::stat_card;
use yahweh_widget_app_header::app_header;
const POLL_INTERVAL: Duration = Duration::from_secs(5);
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
fn main() {
Application::new().run(|cx: &mut App| {
Theme::install_default(cx);
let bounds = Bounds::centered(None, gpui::size(px(720.), px(480.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some(SharedString::from("Brahman Broker — Probe")),
..Default::default()
}),
..Default::default()
},
|_w, cx| cx.new(Explorer::new),
)
.expect("open window");
cx.activate(true);
});
launch_app("Brahman Broker — Probe", (720., 480.), Explorer::new);
}
/// Snapshot de un probe.
+1
View File
@@ -8,6 +8,7 @@ description = "Dashboard GPUI del repo Minga: counts de nodos AST, atestaciones,
[dependencies]
minga-store = { path = "../../modules/semantic_dht/minga-store" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" }
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
+4 -20
View File
@@ -27,36 +27,20 @@ use std::path::PathBuf;
use std::time::Duration;
use gpui::{
div, prelude::*, px, App, Application, Bounds, Context, IntoElement, Render, SharedString,
Window, WindowBounds, WindowOptions,
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
};
use minga_store::PersistentRepo;
use yahweh_launcher::launch_app;
use yahweh_theme::Theme;
use yahweh_widget_app_header::app_header;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_stat_card::stat_card;
use yahweh_widget_app_header::app_header;
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
const REPO_DIRNAME: &str = "repo";
fn main() {
Application::new().run(|cx: &mut App| {
Theme::install_default(cx);
let bounds = Bounds::centered(None, gpui::size(px(800.), px(560.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some(SharedString::from("Minga — Repo")),
..Default::default()
}),
..Default::default()
},
|_w, cx| cx.new(Explorer::new),
)
.expect("open window");
cx.activate(true);
});
launch_app("Minga — Repo", (800., 560.), Explorer::new);
}
/// Cuántos items recientes mostrar por sección. Los stores no
+1
View File
@@ -11,6 +11,7 @@ yahweh-meta-runtime = { path = "../../modules/ui_engine/libs/meta-runtime" }
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
yahweh-widget-card = { path = "../../modules/ui_engine/widgets/card" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
gpui = { workspace = true }
serde_json = { workspace = true }
+4 -23
View File
@@ -26,39 +26,20 @@ use std::path::PathBuf;
use std::time::Duration;
use gpui::{
div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render,
SharedString, Window, WindowBounds, WindowOptions,
div, prelude::*, px, rgb, Context, IntoElement, Render, SharedString, Window,
};
use nakui_core::event_log::{EventLog, LogEntry};
use yahweh_launcher::launch_app;
use yahweh_meta_runtime::{preview_value, short_hash, short_uuid};
use yahweh_theme::Theme;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_app_header::app_header;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_card::card_themed;
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
fn main() {
Application::new().run(|cx: &mut App| {
// Theme global instalado al boot — los widgets themed lo
// requieren, y simplifica el chrome del app a una paleta
// consistente.
Theme::install_default(cx);
let bounds = Bounds::centered(None, gpui::size(px(900.), px(640.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some(SharedString::from("Nakui — Event Log")),
..Default::default()
}),
..Default::default()
},
|_w, cx| cx.new(Explorer::new),
)
.expect("open window");
cx.activate(true);
});
launch_app("Nakui — Event Log", (900., 640.), Explorer::new);
}
/// Estado de la vista. `entries` se reescribe en cada tick (el log
+1
View File
@@ -10,6 +10,7 @@ brahman-card = { path = "../../core/brahman-card" }
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
nouser-card = { path = "../../modules/nouser/card" }
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
yahweh-widget-card = { path = "../../modules/ui_engine/widgets/card" }
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
+4 -22
View File
@@ -23,41 +23,23 @@ use std::time::Duration;
use brahman_sidecar::{await_provider_blocking, build_consumer_card, ConsumerError};
use gpui::{
div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render, SharedString,
Window, WindowBounds, WindowOptions,
div, prelude::*, px, rgb, Context, IntoElement, Render, SharedString, Window,
};
use nouser_card::query::client as query_client;
use nouser_card::query::{transport, ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
use nouser_card::Lens;
use yahweh_launcher::launch_app;
use yahweh_theme::Theme;
use yahweh_widget_app_header::app_header;
use yahweh_widget_banner::{banner_themed, Banner};
use yahweh_widget_card::card_themed;
use yahweh_widget_app_header::app_header;
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
const QUERY_TIMEOUT: Duration = Duration::from_secs(2);
fn main() {
Application::new().run(|cx: &mut App| {
// Theme global instalado al boot — los widgets themed lo
// requieren y unifica el chrome del app.
Theme::install_default(cx);
let bounds = Bounds::centered(None, gpui::size(px(900.), px(640.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(gpui::TitlebarOptions {
title: Some(SharedString::from("Nouser — Mónadas")),
..Default::default()
}),
..Default::default()
},
|_w, cx| cx.new(Explorer::new),
)
.expect("open window");
cx.activate(true);
});
launch_app("Nouser — Mónadas", (900., 640.), Explorer::new);
}
/// Vista raíz: cachea el socket descubierto, el último snapshot y el
@@ -0,0 +1,13 @@
[package]
name = "yahweh-launcher"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "Launcher GPUI reusable: Application::new + Theme::install_default + cx.open_window + cx.activate. Las explorer apps lo invocan en una sola línea (`launch_app(title, size, root_factory)`)."
[dependencies]
gpui = { workspace = true }
yahweh-theme = { path = "../theme" }
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
@@ -0,0 +1,123 @@
//! Yahweh shell — reduce el boot de un app GPUI temed a una línea.
//!
//! Las 4 (próximamente más) apps explorer del repo declaran el mismo
//! patrón: `Application::new + Theme::install_default + cx.open_window
//! + cx.activate(true)`. Sólo varían el título, el tamaño inicial y la
//! fábrica del root entity.
//!
//! Antes (~20 líneas):
//!
//! ```ignore
//! Application::new().run(|cx: &mut App| {
//! Theme::install_default(cx);
//! let bounds = Bounds::centered(None, gpui::size(px(900.), px(640.)), cx);
//! cx.open_window(
//! WindowOptions {
//! window_bounds: Some(WindowBounds::Windowed(bounds)),
//! titlebar: Some(gpui::TitlebarOptions {
//! title: Some(SharedString::from("Nakui — Event Log")),
//! ..Default::default()
//! }),
//! ..Default::default()
//! },
//! |_w, cx| cx.new(Explorer::new),
//! ).expect("open window");
//! cx.activate(true);
//! });
//! ```
//!
//! Ahora (1 línea):
//!
//! ```ignore
//! launch_app("Nakui — Event Log", (900., 640.), Explorer::new);
//! ```
use gpui::{
App, AppContext, Application, Bounds, Context, Render, SharedString, TitlebarOptions,
WindowBounds, WindowOptions, px,
};
use yahweh_theme::Theme;
/// Configuración del primer (y normalmente único) ventana del app.
///
/// `size` es `(ancho, alto)` en píxeles lógicos. La ventana queda
/// centrada en el monitor primario.
pub struct AppLaunchConfig {
pub title: SharedString,
pub size: (f32, f32),
}
impl AppLaunchConfig {
pub fn new(title: impl Into<SharedString>, size: (f32, f32)) -> Self {
Self {
title: title.into(),
size,
}
}
}
/// Levanta un app GPUI con tema instalado y root entity construido.
///
/// El root debe implementar `Render`. La fábrica `root_factory` recibe
/// el `Context<T>` del nuevo entity para que pueda usar `cx.spawn`,
/// suscribirse a eventos, etc — lo mismo que en el patrón directo.
///
/// Bloquea el thread main hasta que se cierre la ventana
/// (`Application::run` no retorna).
pub fn launch_app<T, F>(title: impl Into<SharedString>, size: (f32, f32), root_factory: F)
where
T: Render + 'static,
F: FnOnce(&mut Context<T>) -> T + Send + 'static,
{
launch_app_with(AppLaunchConfig::new(title, size), root_factory);
}
/// Variante que acepta un `AppLaunchConfig` armado afuera. Útil cuando
/// el config se calcula condicionalmente (env var para tamaño, etc).
pub fn launch_app_with<T, F>(config: AppLaunchConfig, root_factory: F)
where
T: Render + 'static,
F: FnOnce(&mut Context<T>) -> T + Send + 'static,
{
Application::new().run(move |cx: &mut App| {
Theme::install_default(cx);
let bounds = Bounds::centered(None, gpui::size(px(config.size.0), px(config.size.1)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(TitlebarOptions {
title: Some(config.title.clone()),
..Default::default()
}),
..Default::default()
},
|_w, cx| cx.new(root_factory),
)
.expect("open window");
cx.activate(true);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_new_normalizes_inputs() {
let c = AppLaunchConfig::new("My App", (800.0, 600.0));
assert_eq!(c.title.as_ref(), "My App");
assert_eq!(c.size, (800.0, 600.0));
}
#[test]
fn config_accepts_owned_string_title() {
let owned = String::from("Owned Title");
let c = AppLaunchConfig::new(owned, (400.0, 300.0));
assert_eq!(c.title.as_ref(), "Owned Title");
}
// No hay test de `launch_app` aquí: bloquea el thread main hasta
// que la ventana se cierre, y en sandbox no hay DISPLAY. La
// cobertura real es que cada explorer app lo invoque y arranque
// (smoke test manual o con DISPLAY).
}