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:
@@ -6,6 +6,33 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-08
|
## 2026-05-08
|
||||||
|
|
||||||
|
### 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 900×640 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` se usa porque GPUI no provee
|
||||||
|
un runtime tokio.
|
||||||
|
- Nuevo helper 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 muestra ~6 cards en vivo, refrescando cada 2s.
|
||||||
|
|
||||||
|
cargo check --workspace: 0 errores, 0 warnings.
|
||||||
|
|
||||||
### feat(nouser): persistencia sled write-through del MonadDb
|
### feat(nouser): persistencia sled write-through del MonadDb
|
||||||
`MonadDb` ahora soporta backend dual:
|
`MonadDb` ahora soporta backend dual:
|
||||||
|
|
||||||
|
|||||||
Generated
+9
@@ -6374,6 +6374,15 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nouser-explorer"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"brahman-admin",
|
||||||
|
"brahman-card",
|
||||||
|
"gpui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nouser-nous"
|
name = "nouser-nous"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ members = [
|
|||||||
"crates/apps/text_viewer",
|
"crates/apps/text_viewer",
|
||||||
"crates/apps/image_viewer",
|
"crates/apps/image_viewer",
|
||||||
"crates/apps/yahweh-shell",
|
"crates/apps/yahweh-shell",
|
||||||
|
"crates/apps/nouser-explorer",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,3 +30,19 @@ pub async fn query(path: impl AsRef<Path>) -> Result<StatusSnapshot, AdminError>
|
|||||||
let snapshot = serde_json::from_str(&line)?;
|
let snapshot = serde_json::from_str(&line)?;
|
||||||
Ok(snapshot)
|
Ok(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Variante sync de [`query`] para callers que no tienen runtime tokio
|
||||||
|
/// (típicamente: GUIs con su propio executor, como GPUI).
|
||||||
|
pub fn query_blocking(path: impl AsRef<Path>) -> Result<StatusSnapshot, AdminError> {
|
||||||
|
use std::io::{BufRead, BufReader as StdBufReader};
|
||||||
|
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||||
|
let stream = StdUnixStream::connect(path)?;
|
||||||
|
let mut reader = StdBufReader::new(stream);
|
||||||
|
let mut line = String::new();
|
||||||
|
let n = reader.read_line(&mut line)?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(AdminError::Empty);
|
||||||
|
}
|
||||||
|
let snapshot = serde_json::from_str(&line)?;
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user