gioser

tawasuyu/brahman

Rust 570TOML 215Markdown 68JSON 198261 Sutichaykuna

feat(shuma): matar procesos + guardar grupos desde la UI

shuma-exec: RunHandle::kill() — el proceso se comparte con su hilo
coordinador (Arc<Mutex<Child>>) para poder terminarlo; los lectores
cierran al cerrarse los pipes. 8 tests (incluye kill de un sleep).

shuma-shell:
- Cada tarjeta de comando en curso (▷) muestra un botón «✕ matar».
- Meta-comando `:save <nombre>` guarda como grupo los comandos
  ejecutados desde el último guardado. El botón «+» del panel [RUN]
  precarga «:save » en el input para nombrarlo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

3bfb42f1cc10338aabc61511d1ce2472bb92004f · ruwaq sergio · (5 killa ñawpaq) · tayta 0fde2aa27 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 11

crates/apps/shuma-shell/src/main.rs

crates/modules/shuma/shuma-exec/src/lib.rs

2 qillqakuna · +125 −16

diff --git a/crates/apps/shuma-shell/src/main.rs b/crates/apps/shuma-shell/src/main.rs
index de0399e..6f66bd1 100644
--- a/crates/apps/shuma-shell/src/main.rs
+++ b/crates/apps/shuma-shell/src/main.rs
@@ -252,6 +252,9 @@ struct Shell {
     drag: Option<Drag>,
     /// Estado de presentación por comando (acordeón, filtro stderr).
     run_ui: HashMap<RunId, RunUi>,
+    /// Largo del historial en el último `:save` — define qué comandos
+    /// entran al próximo grupo guardado.
+    group_anchor: usize,
     /// Scroll del feed central — sigue al comando más reciente.
     scroll: ScrollHandle,
     focus: FocusHandle,
@@ -291,6 +294,7 @@ impl Shell {
             right_width: 188.0,
             drag: None,
             run_ui: HashMap::new(),
+            group_anchor: 0,
             scroll: ScrollHandle::new(),
             focus: cx.focus_handle(),
             focused_once: false,
@@ -381,6 +385,19 @@ impl Shell {
     /// Ejecuta una línea: `cd` se maneja internamente (cambia el cwd y,
     /// con él, el aislamiento); el resto se lanza con `shuma-exec` y su
     /// salida se transmite al panel central.
+    /// Guarda como grupo los comandos ejecutados desde el último `:save`.
+    fn save_group(&mut self, name: &str) {
+        let name = name.trim();
+        if name.is_empty() {
+            return;
+        }
+        let n = self.session.history().len().saturating_sub(self.group_anchor);
+        if n > 0 {
+            self.session.save_recent_as_group(name, n);
+            self.group_anchor = self.session.history().len();
+        }
+    }
+
     fn run_command(&mut self, line: String) {
         let line = line.trim().to_string();
         if line.is_empty() {
@@ -388,6 +405,12 @@ impl Shell {
         }
         let now = unix_now();
 
+        // Meta-comando `:save <nombre>` — guarda un grupo, no se ejecuta.
+        if let Some(name) = line.strip_prefix(":save ") {
+            self.save_group(name);
+            return;
+        }
+
         // Los comandos anteriores que el usuario no fijó se autocolapsan
         // al aparecer uno nuevo abajo — orden de terminal tradicional.
         for ui in self.run_ui.values_mut() {
@@ -425,6 +448,13 @@ impl Shell {
         self.scroll.scroll_to_bottom();
     }
 
+    /// Mata el proceso de un comando en curso.
+    fn kill_run(&self, id: RunId) {
+        if let Some((_, handle)) = self.active.iter().find(|(rid, _)| *rid == id) {
+            handle.kill();
+        }
+    }
+
     fn refresh_completion(&mut self) {
         let comp = self.line.complete(&self.source);
         self.show_completion =
@@ -677,13 +707,37 @@ fn render_run(
         None
     };
 
+    // Botón de matar — sólo mientras el comando sigue corriendo.
+    let kill_chip = if r.status == RunStatus::Running {
+        Some(
+            div()
+                .id(SharedString::from(format!("kill-{id}")))
+                .flex_none()
+                .px(px(6.))
+                .py(px(1.))
+                .rounded(px(3.))
+                .text_size(px(11.))
+                .text_color(gpui::hsla(2.0 / 360.0, 0.66, 0.64, 1.0))
+                .cursor_pointer()
+                .hover(|s| s.bg(gpui::hsla(2.0 / 360.0, 0.55, 0.28, 1.0)))
+                .child("✕ matar")
+                .on_click(cx.listener(move |shell, _, _, cx| {
+                    shell.kill_run(id);
+                    cx.notify();
+                })),
+        )
+    } else {
+        None
+    };
+
     let header = div()
         .flex()
         .flex_row()
         .items_center()
         .gap(px(6.))
         .child(header_left)
-        .children(stderr_chip);
+        .children(stderr_chip)
+        .children(kill_chip);
 
     // Cuerpo: sólo con el acordeón abierto. El filtro elige el flujo.
     let mut body: Vec<gpui::Div> = Vec::new();
@@ -841,16 +895,36 @@ impl Render for Shell {
                         .child(div().text_color(dim).text_size(px(12.)).child("[RUN] grupos"))
                         .child(
                             div()
-                                .id("collapse-left")
-                                .px(px(5.))
-                                .text_color(dim)
-                                .cursor_pointer()
-                                .hover(|s| s.text_color(accent))
-                                .child("«")
-                                .on_click(cx.listener(|s, _, _, cx| {
-                                    s.left_collapsed = true;
-                                    cx.notify();
-                                })),
+                                .flex()
+                                .flex_row()
+                                .gap(px(4.))
+                                .items_center()
+                                .child(
+                                    div()
+                                        .id("save-group")
+                                        .px(px(5.))
+                                        .text_color(dim)
+                                        .cursor_pointer()
+                                        .hover(|s| s.text_color(accent))
+                                        .child("+")
+                                        .on_click(cx.listener(|s, _, _, cx| {
+                                            s.line.set_text(":save ");
+                                            cx.notify();
+                                        })),
+                                )
+                                .child(
+                                    div()
+                                        .id("collapse-left")
+                                        .px(px(5.))
+                                        .text_color(dim)
+                                        .cursor_pointer()
+                                        .hover(|s| s.text_color(accent))
+                                        .child("«")
+                                        .on_click(cx.listener(|s, _, _, cx| {
+                                            s.left_collapsed = true;
+                                            cx.notify();
+                                        })),
+                                ),
                         ),
                 )
                 .children(groups)
@@ -858,7 +932,7 @@ impl Render for Shell {
                     div()
                         .text_size(px(10.))
                         .text_color(dim)
-                        .child("clic para ejecutar el grupo"),
+                        .child("clic ejecuta · + guarda lo último"),
                 )
         };
 
diff --git a/crates/modules/shuma/shuma-exec/src/lib.rs b/crates/modules/shuma/shuma-exec/src/lib.rs
index 504d8b8..07fbc48 100644
--- a/crates/modules/shuma/shuma-exec/src/lib.rs
+++ b/crates/modules/shuma/shuma-exec/src/lib.rs
@@ -17,8 +17,9 @@
 #![forbid(unsafe_code)]
 
 use std::io::{BufRead, BufReader};
-use std::process::{Command, Stdio};
+use std::process::{Child, Command, Stdio};
 use std::sync::mpsc::{self, Receiver, TryRecvError};
+use std::sync::{Arc, Mutex};
 
 /// Qué ejecutar: una línea de comandos, en un directorio, con un shell.
 #[derive(Debug, Clone)]
@@ -63,9 +64,21 @@ impl RunEvent {
 pub struct RunHandle {
     rx: Receiver<RunEvent>,
     finished: bool,
+    /// El proceso, compartido con su hilo coordinador para poder matarlo.
+    child: Arc<Mutex<Option<Child>>>,
 }
 
 impl RunHandle {
+    /// Mata el proceso (envía la señal de terminación). No hace nada si
+    /// el proceso ya terminó o nunca llegó a lanzarse.
+    pub fn kill(&self) {
+        if let Ok(mut guard) = self.child.lock() {
+            if let Some(c) = guard.as_mut() {
+                let _ = c.kill();
+            }
+        }
+    }
+
     /// Drena todos los eventos disponibles ahora mismo, sin bloquear.
     /// Marca el asa como terminada al ver un evento terminal.
     pub fn try_events(&mut self) -> Vec<RunEvent> {
@@ -114,6 +127,8 @@ impl RunHandle {
 pub fn run(spec: &CommandSpec) -> RunHandle {
     let (tx, rx) = mpsc::channel();
     let spec = spec.clone();
+    let child_cell: Arc<Mutex<Option<Child>>> = Arc::new(Mutex::new(None));
+    let cell = Arc::clone(&child_cell);
 
     std::thread::spawn(move || {
         let spawned = Command::new(&spec.shell)
@@ -136,6 +151,10 @@ pub fn run(spec: &CommandSpec) -> RunHandle {
         // Un hilo lector por flujo: stdout y stderr fluyen en paralelo.
         let stdout = child.stdout.take();
         let stderr = child.stderr.take();
+        // Comparte el proceso para que `RunHandle::kill` pueda alcanzarlo.
+        if let Ok(mut g) = cell.lock() {
+            *g = Some(child);
+        }
         let out_reader = stdout.map(|s| {
             let tx = tx.clone();
             std::thread::spawn(move || {
@@ -157,21 +176,24 @@ pub fn run(spec: &CommandSpec) -> RunHandle {
             })
         });
 
+        // Los lectores terminan cuando el proceso cierra sus pipes —sea
+        // por fin natural o por `kill`—; recién entonces se cosecha.
         if let Some(h) = out_reader {
             let _ = h.join();
         }
         if let Some(h) = err_reader {
             let _ = h.join();
         }
-        let code = child
-            .wait()
+        let code = cell
+            .lock()
             .ok()
+            .and_then(|mut g| g.as_mut().and_then(|c| c.wait().ok()))
             .and_then(|s| s.code())
             .unwrap_or(-1);
         let _ = tx.send(RunEvent::Exited(code));
     });
 
-    RunHandle { rx, finished: false }
+    RunHandle { rx, finished: false, child: child_cell }
 }
 
 #[cfg(test)]
@@ -253,4 +275,17 @@ mod tests {
         assert!(RunEvent::Failed("x".into()).is_terminal());
         assert!(!RunEvent::Stdout("x".into()).is_terminal());
     }
+
+    #[test]
+    fn kill_stops_a_long_running_process() {
+        let mut h = run(&sh("sleep 30"));
+        // Espera breve para que el proceso se haya lanzado.
+        std::thread::sleep(std::time::Duration::from_millis(250));
+        h.kill();
+        // wait_all retorna pronto (no espera los 30s) y cierra con un
+        // evento terminal.
+        let events = h.wait_all();
+        assert!(events.last().map(|e| e.is_terminal()).unwrap_or(false));
+        assert!(h.is_finished());
+    }
 }