feat(nouser): yahweh widget — nouser-explorer panel GPUI

Bin GPUI standalone que consulta brahman-admin cada 2s y renderea
todas las sesiones del Init como cards. Cierra el círculo visual
del ecosistema brahman.

- Crate nuevo crates/apps/nouser-explorer (deps: brahman-admin,
  brahman-card, gpui).
- Ventana 900x640 con header del estado del Init, banner de error
  cuando no conecta, y lista de cards (una por sesión).
- Cada card muestra: kind + label + lifecycle, ULID corto, summary
  (si data), keywords, lens hint, service_socket si está, y refs
  (RelationshipKind → target_label). El borde izquierdo coloreado
  diferencia ente (azul) de data (lavanda).
- cx.spawn(async move |this, cx| { ... }) corre el loop de refresh
  en el GPUI executor; query_blocking porque GPUI no tiene runtime
  tokio.
- Helper nuevo en brahman-admin: client::query_blocking(path) —
  versión sync de query(), para callers con su propio executor.

Uso:
  $ ente-zero & nouser daemon crates/core &
  $ cargo run -p nouser-explorer
  # ventana 900x640, ~6 cards en vivo, refrescando cada 2s.

cargo check --workspace: 0 errores, 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 19:54:49 +00:00
parent 7831c0c827
commit 5c41ef920d
6 changed files with 363 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "nouser-explorer"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Explorador GPUI de Mónadas: panel que consulta brahman-admin y renderea las sesiones como cards."
[dependencies]
brahman-admin = { path = "../../core/brahman-admin" }
brahman-card = { path = "../../core/brahman-card" }
gpui = { workspace = true }
[[bin]]
name = "nouser-explorer"
path = "src/main.rs"
+295
View File
@@ -0,0 +1,295 @@
//! `nouser-explorer` — panel GPUI que muestra las Mónadas (y demás
//! sesiones) registradas en el Init brahman.
//!
//! Diseño: ventana standalone que cada N segundos consulta el socket
//! admin (`brahman_admin::client::query_blocking`) y renderea las
//! sesiones como cards. Sin integración con yahweh-shell — es su
//! propio binario para que el ecosistema sea visible incluso sin la
//! shell completa.
//!
//! Uso:
//! ```sh
//! cargo run -p nouser-explorer
//! # con override de socket admin:
//! BRAHMAN_ADMIN_SOCKET=/tmp/brahman-admin.sock cargo run -p nouser-explorer
//! ```
use std::time::Duration;
use brahman_admin::{client::query_blocking, transport, StatusSnapshot};
use brahman_card::CardKind;
use gpui::{
div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render, SharedString,
Window, WindowBounds, WindowOptions,
};
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
fn main() {
Application::new().run(|cx: &mut App| {
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);
});
}
/// Vista raíz: contiene el último snapshot recibido y el último error.
struct Explorer {
snapshot: Option<StatusSnapshot>,
error: Option<SharedString>,
}
impl Explorer {
fn new(cx: &mut Context<Self>) -> Self {
// Loop de refresh: cada `REFRESH_INTERVAL`, query al admin y
// actualiza el modelo. `cx.spawn` corre en el GPUI executor;
// el `query_blocking` sí bloquea pero sólo brevemente — admin
// responde con un snapshot pequeño.
cx.spawn(async move |this, cx| {
let timer = cx.background_executor().clone();
loop {
let path = transport::default_socket_path();
let result = query_blocking(&path);
let _ = this.update(cx, |me, cx| {
match result {
Ok(snap) => {
me.snapshot = Some(snap);
me.error = None;
}
Err(e) => {
me.error = Some(SharedString::from(format!(
"no conectado a {}: {}",
path.display(),
e
)));
}
}
cx.notify();
});
timer.timer(REFRESH_INTERVAL).await;
}
})
.detach();
Self {
snapshot: None,
error: None,
}
}
}
impl Render for Explorer {
fn render(&mut self, _w: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let bg = rgb(0x14171c);
let card_bg = rgb(0x1d2128);
let text_dim = rgb(0x9ba1ad);
let text = rgb(0xe6e8ec);
let accent_ente = rgb(0x88c0d0);
let accent_data = rgb(0xb48ead);
let header_text = match &self.snapshot {
Some(s) => format!(
"Init · protocol={} · attached={} · {} sesión(es){}",
s.protocol_version,
s.init_attached,
s.sessions.len(),
s.current_context
.as_deref()
.map(|c| format!(" · context: {}", c))
.unwrap_or_default()
),
None => "Esperando snapshot del Init brahman…".to_string(),
};
let header = div()
.px(px(16.))
.py(px(12.))
.bg(card_bg)
.border_b_1()
.border_color(rgb(0x2a2f38))
.text_color(text)
.text_size(px(14.))
.child(header_text);
let error_banner = self.error.as_ref().map(|e| {
div()
.px(px(16.))
.py(px(8.))
.bg(rgb(0x4a2020))
.text_color(rgb(0xffd0d0))
.text_size(px(12.))
.child(e.clone())
});
let cards: Vec<gpui::AnyElement> = match &self.snapshot {
None => vec![],
Some(snap) => snap
.sessions
.iter()
.map(|s| {
let (kind_label, accent) = match s.kind {
CardKind::Ente => ("ente", accent_ente),
CardKind::Data => ("data", accent_data),
};
let summary_line = s
.data
.as_ref()
.map(|d| d.summary.clone())
.unwrap_or_default();
let keywords = s
.data
.as_ref()
.map(|d| d.keywords.join(", "))
.unwrap_or_default();
let lens_line = s
.data
.as_ref()
.map(|d| d.presentation_hint.clone())
.filter(|h| !h.is_empty())
.map(|h| format!("lens: {h}"))
.unwrap_or_default();
let sock_line = s
.service_socket
.as_ref()
.map(|p| format!("socket: {}", p.display()))
.unwrap_or_default();
let refs_line = if s.references.is_empty() {
String::new()
} else {
let parts: Vec<String> = s
.references
.iter()
.map(|r| {
format!(
"{:?}{}",
r.kind,
if r.target_label.is_empty() {
"?"
} else {
r.target_label.as_str()
}
)
})
.collect();
format!("refs: {}", parts.join(" "))
};
div()
.flex()
.flex_col()
.p(px(12.))
.mb(px(8.))
.bg(card_bg)
.rounded(px(6.))
.border_l_4()
.border_color(accent)
.gap(px(2.))
.child(
div()
.flex()
.flex_row()
.gap(px(8.))
.items_center()
.child(
div()
.text_color(accent)
.text_size(px(11.))
.child(format!("[{kind_label}]")),
)
.child(
div()
.text_color(text)
.text_size(px(15.))
.child(s.label.clone()),
)
.child(
div()
.text_color(text_dim)
.text_size(px(11.))
.child(format!("{:?}", s.lifecycle)),
),
)
.child(
div()
.text_color(text_dim)
.text_size(px(11.))
.child(format!("id: {}", s.session)),
)
.when(!summary_line.is_empty(), |d| {
d.child(
div()
.text_color(text)
.text_size(px(12.))
.child(summary_line.clone()),
)
})
.when(!keywords.is_empty(), |d| {
d.child(
div()
.text_color(text_dim)
.text_size(px(11.))
.child(format!("keywords: {keywords}")),
)
})
.when(!lens_line.is_empty(), |d| {
d.child(
div()
.text_color(text_dim)
.text_size(px(11.))
.child(lens_line),
)
})
.when(!sock_line.is_empty(), |d| {
d.child(
div()
.text_color(text_dim)
.text_size(px(11.))
.child(sock_line),
)
})
.when(!refs_line.is_empty(), |d| {
d.child(
div()
.text_color(text_dim)
.text_size(px(11.))
.child(refs_line),
)
})
.into_any_element()
})
.collect(),
};
let body = div()
.flex()
.flex_col()
.p(px(16.))
.overflow_hidden()
.children(cards);
div()
.flex()
.flex_col()
.size_full()
.bg(bg)
.child(header)
.when_some(error_banner, |d, b| d.child(b))
.child(body)
}
}