feat(explorer+daemon): discovery dinamico via broker + query socket
Cierra el "explorer encuentra al daemon de forma totalmente dinamica"
del meta-plan. La UI deja de hardcodear el socket admin: descubre al
daemon nouser via MatchEvent::Available del broker y le consulta sus
Monadas directo.
Pipeline end-to-end:
- Daemon publica engine Card con service_socket = $XDG_RUNTIME_DIR/
nouser-engine.sock y flow.output = monad-list:json.
- Daemon binda Unix socket en ese path con listener blocking que
sirve nouser_card::query::QueryRequest::ListMonads, responde
ListMonadsResponse { engine, monads: Vec<MonadView> }.
- Explorer construye consumer Card con flow.input matched,
brahman_sidecar::await_provider_blocking le devuelve el socket,
y nouser_core::engine_socket::client::list_monads lo consulta.
- Cachea el socket; cualquier fallo de query lo invalida y la
proxima iteracion re-descubre.
Wire types nuevos en nouser_card::query:
- QueryRequest::ListMonads
- ListMonadsResponse { engine: EngineInfo, monads: Vec<MonadView> }
- MonadView: proyeccion slim de MonadManifest SIN centroid ni
members (KB que no tienen por que viajar cada poll).
- transport::default_socket_path() con env override.
Listener en nouser_core::engine_socket: spawn_listener + client
blocking con QueryError tipado. 3 tests integracion verdes.
Refactor explorer:
- Drop dep brahman-admin, add brahman-sidecar/nouser-card/nouser-core.
- State: socket cache + snapshot + socket_source informativo.
- TickOutcome enum desacopla la I/O del UI.
Trade-offs: polling 2s (no streaming — broker no empuja Data cards
hoy), re-discovery full en error (discovery es barato).
Tests: 10 (nouser-card +3 query) + 27 (nouser-core +3 engine_socket)
+ 4 (sidecar) verdes. Explorer compila clean.
This commit is contained in:
@@ -3,11 +3,13 @@ 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."
|
||||
description = "Explorador GPUI de Mónadas: panel que descubre al daemon nouser vía broker brahman y consulta sus Mónadas dinámicamente."
|
||||
|
||||
[dependencies]
|
||||
brahman-admin = { path = "../../core/brahman-admin" }
|
||||
brahman-card = { path = "../../core/brahman-card" }
|
||||
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
|
||||
nouser-card = { path = "../../modules/nouser/card" }
|
||||
nouser-core = { path = "../../modules/nouser/core" }
|
||||
gpui = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
//! `nouser-explorer` — panel GPUI que muestra las Mónadas (y demás
|
||||
//! sesiones) registradas en el Init brahman.
|
||||
//! `nouser-explorer` — panel GPUI que descubre al daemon `nouser`
|
||||
//! vía broker brahman y muestra sus Mónadas en vivo.
|
||||
//!
|
||||
//! 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.
|
||||
//! Diseño: ventana standalone que cada N segundos consulta el query
|
||||
//! socket del daemon (`nouser_core::engine_socket::client::list_monads`).
|
||||
//! El path del socket NO está hardcoded — se descubre vía
|
||||
//! `brahman_sidecar::await_provider_blocking` para el flow
|
||||
//! `monad-list:json`. Si el daemon cae, el socket cacheado se invalida
|
||||
//! y la próxima iteración re-descubre.
|
||||
//!
|
||||
//! 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
|
||||
//! # con override del init socket (heredado de brahman-handshake):
|
||||
//! BRAHMAN_INIT_SOCKET=/tmp/init.sock cargo run -p nouser-explorer
|
||||
//! ```
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_admin::{client::query_blocking, transport, StatusSnapshot};
|
||||
use brahman_card::CardKind;
|
||||
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,
|
||||
};
|
||||
use nouser_card::query::{ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
|
||||
use nouser_card::Lens;
|
||||
use nouser_core::engine_socket::client as query_client;
|
||||
|
||||
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| {
|
||||
@@ -44,35 +53,52 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Vista raíz: contiene el último snapshot recibido y el último error.
|
||||
/// Vista raíz: cachea el socket descubierto, el último snapshot y el
|
||||
/// último error. El socket cacheado se invalida ante cualquier fallo
|
||||
/// de query, forzando re-discovery en la próxima iteración.
|
||||
struct Explorer {
|
||||
snapshot: Option<StatusSnapshot>,
|
||||
socket: Option<PathBuf>,
|
||||
snapshot: Option<ListMonadsResponse>,
|
||||
error: Option<SharedString>,
|
||||
/// Última fuente del socket activo: "discovery" (vía broker) o
|
||||
/// "cache" (reusando el de la iteración anterior). Sólo informativo.
|
||||
socket_source: Option<&'static str>,
|
||||
}
|
||||
|
||||
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.
|
||||
// Loop de refresh: cada `REFRESH_INTERVAL`:
|
||||
// 1. Si no tenemos socket cacheado → discovery vía broker.
|
||||
// 2. Si tenemos → query directo. Fallo → invalida cache.
|
||||
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 prior_socket = this
|
||||
.read_with(cx, |me, _| me.socket.clone())
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let result = tick(prior_socket);
|
||||
|
||||
let _ = this.update(cx, |me, cx| {
|
||||
match result {
|
||||
Ok(snap) => {
|
||||
me.snapshot = Some(snap);
|
||||
TickOutcome::Ok { socket, source, snapshot } => {
|
||||
me.socket = Some(socket);
|
||||
me.socket_source = Some(source);
|
||||
me.snapshot = Some(snapshot);
|
||||
me.error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
me.error = Some(SharedString::from(format!(
|
||||
"no conectado a {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
)));
|
||||
TickOutcome::DiscoveryFailed(msg) => {
|
||||
me.socket = None;
|
||||
me.socket_source = None;
|
||||
me.error = Some(SharedString::from(msg));
|
||||
}
|
||||
TickOutcome::QueryFailed(msg) => {
|
||||
// Invalidamos el socket cacheado: la
|
||||
// próxima iteración re-descubre.
|
||||
me.socket = None;
|
||||
me.socket_source = None;
|
||||
me.error = Some(SharedString::from(msg));
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
@@ -83,33 +109,79 @@ impl Explorer {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
socket: None,
|
||||
snapshot: None,
|
||||
error: None,
|
||||
socket_source: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TickOutcome {
|
||||
Ok {
|
||||
socket: PathBuf,
|
||||
source: &'static str,
|
||||
snapshot: ListMonadsResponse,
|
||||
},
|
||||
DiscoveryFailed(String),
|
||||
QueryFailed(String),
|
||||
}
|
||||
|
||||
/// Resuelve el socket (cache o discovery) y consulta `ListMonads`.
|
||||
/// Pensado para correr en background: no toca GPUI, sólo I/O.
|
||||
fn tick(prior_socket: Option<PathBuf>) -> TickOutcome {
|
||||
let (socket, source) = match prior_socket {
|
||||
Some(p) => (p, "cache"),
|
||||
None => match discover() {
|
||||
Ok(p) => (p, "discovery"),
|
||||
Err(e) => return TickOutcome::DiscoveryFailed(format!("discovery: {e}")),
|
||||
},
|
||||
};
|
||||
|
||||
match query_client::list_monads(&socket, QUERY_TIMEOUT) {
|
||||
Ok(resp) => TickOutcome::Ok {
|
||||
socket,
|
||||
source,
|
||||
snapshot: resp,
|
||||
},
|
||||
Err(e) => TickOutcome::QueryFailed(format!(
|
||||
"query a {}: {e} — re-descubriendo en próxima iteración",
|
||||
socket.display()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovery del daemon vía broker brahman. Construye un consumer
|
||||
/// Card con `flow.input = monad-list:json`, espera al primer
|
||||
/// `MatchEvent::Available`, devuelve el `producer_service_socket`.
|
||||
fn discover() -> Result<PathBuf, ConsumerError> {
|
||||
let card = build_consumer_card("nouser-explorer", FLOW_MONAD_LIST, FLOW_TYPE_NAME);
|
||||
await_provider_blocking(card, DISCOVERY_TIMEOUT)
|
||||
}
|
||||
|
||||
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_engine = 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
|
||||
let header_text = match (&self.snapshot, &self.socket, self.socket_source) {
|
||||
(Some(s), Some(sock), Some(src)) => format!(
|
||||
"Engine '{}' · {} mónada(s) · socket: {} ({}){}",
|
||||
s.engine.label,
|
||||
s.monads.len(),
|
||||
sock.display(),
|
||||
src,
|
||||
s.engine
|
||||
.watching
|
||||
.as_deref()
|
||||
.map(|c| format!(" · context: {}", c))
|
||||
.map(|w| format!(" · watching: {}", w))
|
||||
.unwrap_or_default()
|
||||
),
|
||||
None => "Esperando snapshot del Init brahman…".to_string(),
|
||||
_ => "Buscando daemon nouser vía brahman-broker…".to_string(),
|
||||
};
|
||||
|
||||
let header = div()
|
||||
@@ -134,62 +206,11 @@ impl Render for Explorer {
|
||||
|
||||
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(" "))
|
||||
};
|
||||
Some(snap) => {
|
||||
let mut out = Vec::with_capacity(snap.monads.len() + 1);
|
||||
|
||||
// Engine card primero — el "ser" que owns las Mónadas.
|
||||
out.push(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -198,7 +219,7 @@ impl Render for Explorer {
|
||||
.bg(card_bg)
|
||||
.rounded(px(6.))
|
||||
.border_l_4()
|
||||
.border_color(accent)
|
||||
.border_color(accent_engine)
|
||||
.gap(px(2.))
|
||||
.child(
|
||||
div()
|
||||
@@ -208,72 +229,131 @@ impl Render for Explorer {
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.text_color(accent)
|
||||
.text_color(accent_engine)
|
||||
.text_size(px(11.))
|
||||
.child(format!("[{kind_label}]")),
|
||||
.child("[engine]"),
|
||||
)
|
||||
.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(snap.engine.label.clone()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(text_dim)
|
||||
.text_size(px(11.))
|
||||
.child(format!("id: {}", s.session)),
|
||||
.child(format!("id: {}", snap.engine.id)),
|
||||
)
|
||||
.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| {
|
||||
.when_some(snap.engine.watching.clone(), |d, w| {
|
||||
d.child(
|
||||
div()
|
||||
.text_color(text_dim)
|
||||
.text_size(px(11.))
|
||||
.child(format!("keywords: {keywords}")),
|
||||
.child(format!("watching: {w}")),
|
||||
)
|
||||
})
|
||||
.when(!lens_line.is_empty(), |d| {
|
||||
d.child(
|
||||
.into_any_element(),
|
||||
);
|
||||
|
||||
// Mónadas (kind=Data por construcción).
|
||||
for m in &snap.monads {
|
||||
let lens = lens_label(m.dominant_lens);
|
||||
let keywords = m.keywords.join(", ");
|
||||
let path_hint_line = m
|
||||
.path_hint
|
||||
.as_deref()
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(|p| format!("path: {p}"));
|
||||
let model_line = m
|
||||
.centroid_model
|
||||
.as_deref()
|
||||
.filter(|m| !m.is_empty())
|
||||
.map(|m| format!("model: {m}"));
|
||||
|
||||
out.push(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p(px(12.))
|
||||
.mb(px(8.))
|
||||
.bg(card_bg)
|
||||
.rounded(px(6.))
|
||||
.border_l_4()
|
||||
.border_color(accent_data)
|
||||
.gap(px(2.))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap(px(8.))
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.text_color(accent_data)
|
||||
.text_size(px(11.))
|
||||
.child("[monad]"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(text)
|
||||
.text_size(px(15.))
|
||||
.child(m.label.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(text_dim)
|
||||
.text_size(px(11.))
|
||||
.child(format!(
|
||||
"{} files · ent {:.2} · {}",
|
||||
m.cardinality, m.entropy, lens
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(text_dim)
|
||||
.text_size(px(11.))
|
||||
.child(lens_line),
|
||||
.child(format!("id: {}", m.id)),
|
||||
)
|
||||
})
|
||||
.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(),
|
||||
.when(!m.summary.is_empty(), |d| {
|
||||
d.child(
|
||||
div()
|
||||
.text_color(text)
|
||||
.text_size(px(12.))
|
||||
.child(m.summary.clone()),
|
||||
)
|
||||
})
|
||||
.when(!keywords.is_empty(), |d| {
|
||||
d.child(
|
||||
div()
|
||||
.text_color(text_dim)
|
||||
.text_size(px(11.))
|
||||
.child(format!("keywords: {keywords}")),
|
||||
)
|
||||
})
|
||||
.when_some(path_hint_line, |d, line| {
|
||||
d.child(
|
||||
div()
|
||||
.text_color(text_dim)
|
||||
.text_size(px(11.))
|
||||
.child(line),
|
||||
)
|
||||
})
|
||||
.when_some(model_line, |d, line| {
|
||||
d.child(
|
||||
div()
|
||||
.text_color(text_dim)
|
||||
.text_size(px(11.))
|
||||
.child(line),
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
};
|
||||
|
||||
let body = div()
|
||||
@@ -293,3 +373,14 @@ impl Render for Explorer {
|
||||
.child(body)
|
||||
}
|
||||
}
|
||||
|
||||
fn lens_label(l: Lens) -> &'static str {
|
||||
match l {
|
||||
Lens::Grid => "grid",
|
||||
Lens::Code => "code",
|
||||
Lens::Gallery => "gallery",
|
||||
Lens::Database => "database",
|
||||
Lens::Markdown => "markdown",
|
||||
Lens::Tree => "tree",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user