mirror of
https://github.com/block/buzz.git
synced 2026-11-05 00:41:28 +00:00
## Summary - ship **Buzz Term** end to end: the terminal engine/runtime, mounted desktop substrate, and user-visible naming - add Quinn's tape-deck-inspired banner: a beveled chassis filled by the `buzz term` wordmark, surrounded by a complete-hex field - derive the wordmark's three-stop sweep from each theme's terminal palette so primary, secondary, and accent roles remain visibly distinct across all 62 shipped themes, including light themes - paint the banner once on its own pointer-transparent canvas; PTY rendering beneath it remains unchanged ## Banner behavior - uses the renderer's shared `8.4 × 17` cell metrics and production aspect ratio `2.0238` - regenerates only for viewport/theme changes; palette switches repaint correctly while the banner is visible - dismisses on non-empty output from the active terminal session; empty output and inactive sessions do not dismiss it - fails closed below **70 columns** rather than squeezing or clipping the wordmark - adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and **zero lines inside `paint()`** ## Screenshots | Buzz (light) | Buzz Dark | |---|---| |  |  | | Kanagawa Lotus (light) | Red | |---|---| |  |  | Additional production-aspect finals: [Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png), [Min Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png), and [Dark Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png). The screenshot harness was checked against the shipped painter at this exact head: all **2,541 draw calls** matched on color, glyph, x, and y; four deliberate divergence controls fired. ## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd` - desktop tests: **3,946 / 3,946** - TypeScript: clean - checks: pass (two pre-existing informational `useTemplate` notices only) - integration/e2e: PASS (independent exact-SHA lane; artifacts recorded in the originating Buzz thread) - artifact/dead-path sweep: clean - redteam G1–G7: PASS - all six named banner emitter-deletion mutants die - independent handwritten five-row full-wordmark fixture kills Quinn's seven-mutant battery, including a one-pixel glyph change - real `112 × 46` canvas-rect dismissal tests separately cover active non-empty, active empty, and inactive non-empty output - layer-drop and zero-draw painter mutants die; z-order and pointer-events verified - CI's `tsc && vite build` includes all three banner modules - performance at DPR 2 (worst-case measured envelope): - one-time content paint: **~0.7–0.8 ms**, paid only when the banner is built or its palette changes - busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497 µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame) - busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`: **1,139–1,212 µs/frame** (**6.83–7.27%**) - empty, one-glyph, and full-banner controls converge: compositor cost follows backing-layer area and DPR rather than painted-cell count - in the actual idle welcome state, cost is below both vsync-clamped rigs' resolution; it is not claimed as zero - **Pane cross-rig spread: resolved at matched loop rate.** Two independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of *that* spread is rAF loop rate: the higher figure came from a free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read 58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures quoted above remain the **unthrottled worst case** and are conservative by ~2.3× at the pane. Not established: the mechanism and sign of free-running distortion (one rig under-charges ~15%, the other over-charges 2.3×), and the 1080p figure has not been re-measured throttled. - the layer paints only on generation/theme/resize and dismisses on first non-empty active-session output, so the measurable busy cost is a short-lived worst case rather than a persistent PTY paint-path tax ## Follow-ups in this PR These are intentionally subsequent commits after the certified static-banner head, not claims about `98ebc8f90`: 1. close the compositor metrology: remeasure the 1080p point throttled and characterize the opposite-sign free-running rAF distortion, with each measurement regime stated 2. add Tyler's animated honeycomb color waves, gated by `prefers-reduced-motion`, a full 62-theme phase-sweep contrast check, and DPR-2 per-tick performance certification 3. land the already-proven mounted theme-switch regression probe from `RESEARCH/BUZZ_TERM_G3A_PROBE/` 4. bound the slow/hang-shaped G1-c mutant `waitFor` 5. optionally trim the generator to its ink bounding box, reducing the minimum viewport from 70 to 62 columns --------- Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
281 lines
10 KiB
Rust
281 lines
10 KiB
Rust
use tauri::Manager;
|
||
|
||
use crate::app_state::AppState;
|
||
use crate::managed_agents::{
|
||
self, kill_stale_tracked_processes, load_managed_agents, save_managed_agents,
|
||
sync_managed_agent_processes, BackendKind,
|
||
};
|
||
use crate::{prevent_sleep, util};
|
||
|
||
pub(crate) fn is_restart_request(code: Option<i32>) -> bool {
|
||
code == Some(tauri::RESTART_EXIT_CODE)
|
||
}
|
||
|
||
pub(crate) fn shut_down_app(app: &tauri::AppHandle, shutdown_done: &std::sync::atomic::AtomicBool) {
|
||
use std::sync::atomic::Ordering;
|
||
|
||
app.state::<AppState>()
|
||
.shutdown_started
|
||
.store(true, Ordering::SeqCst);
|
||
if !shutdown_done.swap(true, Ordering::SeqCst) {
|
||
prevent_sleep::release(&app.state::<AppState>().prevent_sleep);
|
||
app.state::<crate::terminal_runtime::TerminalSessions>()
|
||
.shutdown_all();
|
||
if let Err(error) = shutdown_managed_agents(app) {
|
||
eprintln!("buzz-desktop: failed to stop managed agents: {error}");
|
||
}
|
||
#[cfg(feature = "mesh-llm")]
|
||
shutdown_mesh_runtime(app);
|
||
}
|
||
}
|
||
|
||
/// Install SIGINT/SIGTERM/SIGHUP cleanup on ctrlc's dedicated handler thread.
|
||
#[cfg(unix)]
|
||
pub(crate) fn install_signal_handler(
|
||
app: tauri::AppHandle,
|
||
shutdown_done: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||
) {
|
||
use std::sync::atomic::Ordering;
|
||
|
||
if let Err(error) = ctrlc::set_handler(move || {
|
||
app.state::<AppState>()
|
||
.shutdown_started
|
||
.store(true, Ordering::SeqCst);
|
||
if !shutdown_done.swap(true, Ordering::SeqCst) {
|
||
app.state::<crate::terminal_runtime::TerminalSessions>()
|
||
.shutdown_all();
|
||
let _ = shutdown_managed_agents(&app);
|
||
#[cfg(feature = "mesh-llm")]
|
||
shutdown_mesh_runtime(&app);
|
||
}
|
||
#[cfg(all(feature = "mesh-llm", target_os = "macos"))]
|
||
hard_exit_after_mesh_shutdown();
|
||
#[cfg(not(all(feature = "mesh-llm", target_os = "macos")))]
|
||
std::process::exit(0);
|
||
}) {
|
||
eprintln!("buzz-desktop: failed to register signal handler: {error}");
|
||
}
|
||
}
|
||
|
||
#[cfg(all(feature = "mesh-llm", target_os = "macos"))]
|
||
fn updated_macos_binary(current_binary: &std::path::Path) -> Option<std::path::PathBuf> {
|
||
let macos_directory = current_binary.parent()?;
|
||
if macos_directory.file_name()? != "MacOS" {
|
||
return None;
|
||
}
|
||
let contents_directory = macos_directory.parent()?;
|
||
if contents_directory.file_name()? != "Contents" {
|
||
return None;
|
||
}
|
||
let info_plist =
|
||
plist::from_file::<_, plist::Dictionary>(contents_directory.join("Info.plist")).ok()?;
|
||
let binary_name = info_plist.get("CFBundleExecutable")?.as_string()?;
|
||
Some(macos_directory.join(binary_name))
|
||
}
|
||
|
||
#[cfg(all(feature = "mesh-llm", target_os = "macos"))]
|
||
pub(crate) fn relaunch_after_mesh_shutdown(app: &tauri::AppHandle) -> ! {
|
||
use std::process::Command;
|
||
|
||
tauri_plugin_single_instance::destroy(app);
|
||
let env = app.env();
|
||
match tauri::process::current_binary(&env) {
|
||
Ok(current_binary) => {
|
||
let binary = updated_macos_binary(¤t_binary).unwrap_or(current_binary);
|
||
if let Err(error) = Command::new(binary)
|
||
.args(env.args_os.iter().skip(1))
|
||
.spawn()
|
||
{
|
||
eprintln!("buzz-desktop: failed to relaunch app: {error}");
|
||
}
|
||
}
|
||
Err(error) => eprintln!("buzz-desktop: failed to locate app for relaunch: {error}"),
|
||
}
|
||
hard_exit_after_mesh_shutdown();
|
||
}
|
||
|
||
#[cfg(all(feature = "mesh-llm", target_os = "macos"))]
|
||
pub(crate) fn hard_exit_after_mesh_shutdown() -> ! {
|
||
// SAFETY: all Buzz-managed subprocesses and the embedded Mesh runtime have
|
||
// been stopped. `_exit` intentionally skips only process-global C++
|
||
// destructors and buffered stdio; no application state remains observable.
|
||
unsafe { libc::_exit(0) }
|
||
}
|
||
|
||
#[cfg(feature = "mesh-llm")]
|
||
pub(crate) fn shutdown_mesh_runtime(app: &tauri::AppHandle) {
|
||
let app = app.clone();
|
||
let (tx, rx) = std::sync::mpsc::channel();
|
||
tauri::async_runtime::spawn(async move {
|
||
let state = app.state::<AppState>();
|
||
let runtime = state.mesh_llm_runtime.lock().await.take();
|
||
let result = match runtime {
|
||
Some(runtime) => runtime.stop().await,
|
||
None => Ok(()),
|
||
};
|
||
let _ = tx.send(result);
|
||
});
|
||
match rx.recv_timeout(std::time::Duration::from_secs(5)) {
|
||
Ok(Ok(())) => {}
|
||
Ok(Err(error)) => eprintln!("buzz-desktop: failed to stop Mesh runtime: {error}"),
|
||
Err(error) => eprintln!("buzz-desktop: timed out stopping Mesh runtime: {error}"),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn shutdown_managed_agents(app: &tauri::AppHandle) -> Result<(), String> {
|
||
let state = app.state::<AppState>();
|
||
let _restore_transition = state
|
||
.managed_agent_runtime_transition
|
||
.lock()
|
||
.map_err(|error| error.to_string())?;
|
||
let _store_guard = state
|
||
.managed_agents_store_lock
|
||
.lock()
|
||
.map_err(|error| error.to_string())?;
|
||
let mut records = load_managed_agents(app)?;
|
||
let mut runtimes = state
|
||
.managed_agent_processes
|
||
.lock()
|
||
.map_err(|error| error.to_string())?;
|
||
let (mut changed, _exited) = sync_managed_agent_processes(
|
||
&mut records,
|
||
&mut runtimes,
|
||
&managed_agents::current_instance_id(app),
|
||
);
|
||
changed |= kill_stale_tracked_processes(
|
||
&mut records,
|
||
&runtimes,
|
||
&managed_agents::current_instance_id(app),
|
||
);
|
||
|
||
// Stop all tracked agents. Send SIGTERM to all process
|
||
// groups first, then wait for exits in parallel to avoid serial 1s waits.
|
||
struct AgentToStop {
|
||
idx: usize,
|
||
pid: u32,
|
||
runtime: Option<managed_agents::ManagedAgentPairRuntime>,
|
||
}
|
||
|
||
let mut to_stop: Vec<AgentToStop> = Vec::new();
|
||
for (idx, record) in records.iter().enumerate() {
|
||
if record.backend != BackendKind::Local {
|
||
continue;
|
||
}
|
||
// Drain every tracked pair for this record, not just the first — an
|
||
// agent can run one harness per community, and each pair gets the
|
||
// graceful SIGTERM → 2s wait → SIGKILL fan-out with a stop log
|
||
// marker, instead of falling through to the orphan sweep's 200ms
|
||
// grace below.
|
||
for key in managed_agents::managed_agent_runtime_keys(&runtimes, &record.pubkey) {
|
||
let runtime = runtimes.remove(&key);
|
||
let Some(pid) = runtime
|
||
.as_ref()
|
||
.map(|rt| rt.child.id())
|
||
.or(record.runtime_pid)
|
||
else {
|
||
continue;
|
||
};
|
||
to_stop.push(AgentToStop { idx, pid, runtime });
|
||
}
|
||
}
|
||
|
||
if !to_stop.is_empty() {
|
||
changed = true;
|
||
|
||
// Fan-out: send SIGTERM to all process groups at once.
|
||
#[cfg(unix)]
|
||
for agent in &to_stop {
|
||
let pgid = -(agent.pid as i32);
|
||
unsafe {
|
||
libc::kill(pgid, libc::SIGTERM);
|
||
}
|
||
}
|
||
|
||
// Wait up to 2s for all to exit, checking in a polling loop.
|
||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
|
||
loop {
|
||
if to_stop
|
||
.iter()
|
||
.all(|a| !managed_agents::process_is_running(a.pid))
|
||
{
|
||
break;
|
||
}
|
||
if std::time::Instant::now() >= deadline {
|
||
break;
|
||
}
|
||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||
}
|
||
|
||
// Fan-out: SIGKILL any survivors.
|
||
#[cfg(unix)]
|
||
for agent in &to_stop {
|
||
if managed_agents::process_is_running(agent.pid) {
|
||
let pgid = -(agent.pid as i32);
|
||
unsafe {
|
||
libc::kill(pgid, libc::SIGKILL);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Reap children and update records.
|
||
for mut agent in to_stop {
|
||
if let Some(ref mut rt) = agent.runtime {
|
||
// Best-effort reap — don’t block shutdown if the child is stuck
|
||
// in uninterruptible sleep. The zombie will be cleaned up when
|
||
// our process exits and launchd reaps it.
|
||
let _ = rt.child.try_wait();
|
||
// Write log marker (best-effort).
|
||
let record = &records[agent.idx];
|
||
let _ = managed_agents::append_log_marker(
|
||
&rt.log_path,
|
||
&format!(
|
||
"=== stopped {} ({}) at {} ===",
|
||
record.name,
|
||
record.pubkey,
|
||
util::now_iso()
|
||
),
|
||
);
|
||
}
|
||
let record = &mut records[agent.idx];
|
||
record.runtime_pid = None;
|
||
record.last_stopped_at = Some(util::now_iso());
|
||
record.updated_at = util::now_iso();
|
||
record.last_exit_code = None;
|
||
record.last_error = None;
|
||
}
|
||
}
|
||
|
||
// Final sweep: kill any orphaned agent processes we have PID file receipts
|
||
// for that escaped process-group kills or weren't tracked in records.
|
||
// All tracked PIDs have already been killed above, so pass an empty skip list.
|
||
managed_agents::sweep_orphaned_agent_processes(app, &[]);
|
||
|
||
// System-wide sweep: agent workers (goose, buzz-agent, etc.) are spawned
|
||
// in their own process groups by buzz-acp, so group-kills above only
|
||
// reach the harness, not the workers. Scan all user processes and kill any
|
||
// known agent binaries that are still running.
|
||
managed_agents::sweep_system_agent_processes(&managed_agents::current_instance_id(app), &[]);
|
||
|
||
// Dead-instance reaping: find agents belonging to Buzz instances
|
||
// whose desktop process is no longer running and reap them.
|
||
managed_agents::reap_dead_instance_agents(&managed_agents::current_instance_id(app), &[]);
|
||
|
||
if changed {
|
||
save_managed_agents(app, &records)?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::is_restart_request;
|
||
|
||
#[test]
|
||
fn only_tauri_restart_exit_code_requests_a_relaunch() {
|
||
assert!(is_restart_request(Some(tauri::RESTART_EXIT_CODE)));
|
||
assert!(!is_restart_request(None));
|
||
assert!(!is_restart_request(Some(0)));
|
||
}
|
||
}
|