Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
4673db1
fix: found tailscale CLI outside the minimal GUI-app PATH
R0MADEV Aug 23, 2026
b262950
Merge branch 'main' of github.com:R0MADEV/bento
R0MADEV Aug 23, 2026
463f4c0
feat: added raw-mode interactivity to bento attach
R0MADEV Aug 24, 2026
5ef7dfe
feat: added review.branches command to the CLI and daemon IPC
R0MADEV Aug 24, 2026
590d78b
feat: added review.prs command to the CLI and daemon IPC
R0MADEV Aug 24, 2026
3f6e785
feat: added review.files command to the CLI and daemon IPC
R0MADEV Aug 24, 2026
d207921
feat: added review.pr_diff and review.pr_comments commands
R0MADEV Aug 24, 2026
785a8a4
feat: added PR comment CRUD and review submit commands
R0MADEV Aug 24, 2026
7cf570d
feat: added review.ask streaming command to the CLI and daemon IPC
R0MADEV Aug 24, 2026
3a3dc9b
fix: stopped forwarding ask()'s [DONE] sentinel as visible review.output
R0MADEV Aug 24, 2026
28c2f6b
feat: added a real full-screen TUI panel (bento with no args)
R0MADEV Aug 24, 2026
4cd76c5
feat: added review.run command to the CLI and daemon IPC
R0MADEV Aug 24, 2026
d753d83
feat: added a Review tab to the TUI panel
R0MADEV Aug 24, 2026
92caf71
feat: added branch/PR browsing, agent picker and cancel to the Review…
R0MADEV Aug 24, 2026
2d1724b
feat: per-file diff view, PR comment/review actions, and a real revie…
R0MADEV Aug 24, 2026
fc5233d
feat: multi-agent compare, review context, checkpoint history, codex …
R0MADEV Aug 24, 2026
56e20b7
refactor: redesigned Review tab as a persistent split-pane browser
R0MADEV Aug 24, 2026
efefcbb
feat: added a project picker to the TUI Review tab
R0MADEV Aug 24, 2026
1b110f9
refactor: extracted the review prompt into a shared bento-review crate
R0MADEV Aug 24, 2026
fd36e01
refactor: moved the git/gh review helpers into bento-review
R0MADEV Aug 24, 2026
c2cc999
refactor: unified the PR operations in bento-review
R0MADEV Aug 24, 2026
0fb96b5
fix: trimmed the project path from the left in the Review sidebar
R0MADEV Aug 24, 2026
d603735
refactor: unified the agent runners in bento-review
R0MADEV Aug 24, 2026
e7efd0f
refactor: split the TUI Review tab into state, input, drawing and format
R0MADEV Aug 24, 2026
4a06835
refactor: moved the review engine into bento-review with typed events
R0MADEV Aug 24, 2026
cfc2349
feat: moved the desktop's review checkpoints to the shared store
R0MADEV Aug 24, 2026
52b9e17
refactor: put the review Tauri calls in the panel and the tests in th…
R0MADEV Aug 24, 2026
53d697a
refactor: sorted src/ui into views, helpers and state
R0MADEV Aug 24, 2026
0c7be99
feat: added a file-size check with a baseline for the existing debt
R0MADEV Aug 24, 2026
2a45511
feat: the TUI remembers which files you already reviewed
R0MADEV Aug 24, 2026
df20395
feat: refresh, PR header and reviewing a branch in the TUI
R0MADEV Aug 24, 2026
8d06da4
refactor: pulled the chat API loop and the clamp maths out of aiChat.ts
R0MADEV Aug 24, 2026
2c046f0
refactor: moved the AI chat DOM into its own module
R0MADEV Aug 24, 2026
afe908a
refactor: split memory.rs into commands, database and validation
R0MADEV Aug 24, 2026
8c6ab85
refactor: split the phone review UI and the daemon's asset serving
R0MADEV Aug 24, 2026
4616c58
refactor: split the CLI entry point into dispatch, service and transport
R0MADEV Aug 24, 2026
59069c9
refactor: split the agent module into commands and the run loop
R0MADEV Aug 24, 2026
eff41d2
refactor: moved the HTTP commands and the native menu out of main.rs
R0MADEV Aug 24, 2026
3b57673
refactor: split memory_sources.rs into sources and scanning
R0MADEV Aug 24, 2026
dc885e5
refactor: split ReviewPanel into state, navigation, CI popover and views
R0MADEV Aug 24, 2026
c167f3b
refactor: split the memory sources view into dom, list and preview
R0MADEV Aug 24, 2026
d0ea1f5
refactor: moved the task diff view out of tasksDetailViews
R0MADEV Aug 24, 2026
036587f
refactor: split the task list row and the rebase editor into their ow…
R0MADEV Aug 24, 2026
699956a
refactor: moved the file diff renderers out of ReviewDiffView
R0MADEV Aug 24, 2026
4cc2dfe
refactor: split the Jira panel into cards, account form and account list
R0MADEV Aug 24, 2026
eb30287
refactor: moved the devcontainer views out of TaskDockerView
R0MADEV Aug 24, 2026
157241d
refactor: pulled session capture and renaming out of AgentsPanel
R0MADEV Aug 24, 2026
b4e4932
refactor: split aiChat.ts — the last file over the size budget
R0MADEV Aug 24, 2026
0c8e411
refactor: put today's new files where the architecture says they go
R0MADEV Aug 24, 2026
b0625c8
refactor: grouped src-tauri by domain instead of by loose file
R0MADEV Aug 24, 2026
41c40a1
refactor: separated the review commands from the worktree logic
R0MADEV Aug 24, 2026
1ea2958
refactor: separated the vault commands from its crypto
R0MADEV Aug 24, 2026
a12570f
refactor: separated the terminal commands from the daemon client
R0MADEV Aug 24, 2026
864bdfc
refactor: moved the panels' pure logic into core
R0MADEV Aug 24, 2026
58f7f3a
refactor: moved startAgent out of core into an adapter
R0MADEV Aug 25, 2026
358f1e7
test: covered the modules extracted from the big files
R0MADEV Aug 25, 2026
f5342eb
feat: made the i18n audit enforceable with a baseline
R0MADEV Aug 25, 2026
556bbc4
feat: taught the i18n audit about the panel accessors, and translated…
R0MADEV Aug 25, 2026
ae2b9ca
feat: CI checks and inline comments in the TUI's PR view
R0MADEV Aug 25, 2026
b108428
feat: search in the TUI review, and filtering the branch list
R0MADEV Aug 25, 2026
37a50bc
feat: the TUI and the phone now review in an isolated worktree too
R0MADEV Aug 25, 2026
85d2fae
feat: the engine retries a transient failure and reports which tool t…
R0MADEV Aug 25, 2026
71e4945
feat: tasks (worktrees) readable from the CLI and the phone, with a w…
R0MADEV Aug 25, 2026
835e012
feat: tasks in the CLI, with their logic in the shared crate
R0MADEV Aug 25, 2026
1f0c9e0
feat: interactive rebase and backups in the shared crate, and in the CLI
R0MADEV Aug 25, 2026
bbe750a
fix: the TypeScript types follow the Rust structs again
R0MADEV Aug 25, 2026
7d7cce8
refactor: the desktop's git module stops duplicating the crate
R0MADEV Aug 25, 2026
36f6b10
feat: docker in the shared crate, and in the CLI and the phone
R0MADEV Aug 25, 2026
83c3821
refactor: moved compose_yaml, devcontainer, isolate, port_probe and s…
R0MADEV Aug 25, 2026
88e6360
feat: devcontainer isolation moved into the crate and exposed over IP…
R0MADEV Aug 25, 2026
1483806
feat: notes moved into a shared crate and exposed over IPC and the CLI
R0MADEV Aug 25, 2026
9ce756c
refactor: the database explorer moved into the bento-db crate
R0MADEV Aug 25, 2026
f3fc31b
feat: memory moved into a shared crate and exposed over IPC and the CLI
R0MADEV Aug 25, 2026
c8a7481
refactor: moved every git command into bento-review, leaving the desk…
R0MADEV Aug 25, 2026
2c52abd
feat: exposed the task status, diff and log over IPC and the CLI
R0MADEV Aug 25, 2026
a7fb7c1
refactor: moved the review document format out of TypeScript into ben…
R0MADEV Aug 25, 2026
486cf8b
feat: agent sessions moved into the bento-sessions crate and reachabl…
R0MADEV Aug 25, 2026
3cfc0c1
refactor: moved SQL building out of TypeScript into bento-db
R0MADEV Aug 25, 2026
62df8ed
fix: the same memory import rule now runs from Rust and from the panel
R0MADEV Aug 25, 2026
391a724
fix: one rule for how a PR check stands, instead of three
R0MADEV Aug 25, 2026
47f8f27
refactor: the fixup ranking and the conflict codes moved to bento-review
R0MADEV Aug 25, 2026
7f9989e
feat: exposed the fixup ranking over IPC and the CLI
R0MADEV Aug 25, 2026
9ee0979
refactor: building the partial patch moved next to the code that appl…
R0MADEV Aug 25, 2026
5b55086
feat: translated the Jira and phone panels, and the last strings that…
R0MADEV Aug 25, 2026
2303a8a
refactor: one list of agents, instead of the six that had drifted apart
R0MADEV Aug 25, 2026
317a9fa
fix: the session-end hook no longer hangs, nor leaves the summarizer …
R0MADEV Aug 25, 2026
49629dd
fix: pinned the locale in tests, which is why CI failed and local did…
R0MADEV Aug 25, 2026
101a756
fix: made the summarizer tests portable and stopped coverage from wal…
R0MADEV Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1,566 changes: 1,535 additions & 31 deletions daemon/Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# untouched during Phase 1. Phase 2 wires the Tauri app to bento-core.
[workspace]
resolver = "2"
members = ["bento-core", "bento-daemon", "bento-cli"]
members = ["bento-core", "bento-db", "bento-docker", "bento-memory", "bento-notes", "bento-review", "bento-sessions", "bento-daemon", "bento-cli"]
10 changes: 9 additions & 1 deletion daemon/bento-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,13 @@ name = "bento"
path = "src/main.rs"

[dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "io-std", "macros", "time"] }
tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "io-std", "macros", "time", "signal", "sync"] }
serde_json = "1.0"
ratatui = "0.30"
crossterm = { version = "0.29", features = ["event-stream"] }
tokio-stream = { version = "0.1", default-features = false }
bento-review = { path = "../bento-review" }
bento-sessions = { path = "../bento-sessions" }

[target.'cfg(unix)'.dependencies]
libc = "0.2"
265 changes: 265 additions & 0 deletions daemon/bento-cli/src/attach.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
//! Interactive `bento attach` — puts the local terminal in raw mode and
//! byte-forwards stdin/stdout against a remote PTY over the daemon's IPC
//! socket, instead of the old line-buffered passthrough.

use serde_json::{json, Value};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;

#[cfg(unix)]
use std::os::unix::io::RawFd;
#[cfg(unix)]
use tokio::io::AsyncReadExt;
#[cfg(unix)]
use tokio::signal::unix::{signal, SignalKind};
#[cfg(unix)]
use tokio::sync::{mpsc, oneshot};

/// Attach to a terminal: stream its output to stdout and forward stdin to it.
/// On unix with a real tty, this is fully interactive (raw mode, live
/// resize, signals pass through to the remote process). Anything else
/// (piped stdin, non-unix) falls back to the old line-buffered behavior.
pub async fn attach(id: &str) -> std::io::Result<()> {
#[cfg(unix)]
{
if unsafe { libc::isatty(0) } != 0 {
return attach_unix(id).await;
}
}
attach_fallback(id).await
}

/// Splits `bytes` at the last point where everything before it is valid
/// UTF-8, so a `read()` that lands mid multi-byte character doesn't get
/// mangled into `U+FFFD` — the incomplete tail is left for the caller to
/// prepend to the next read.
fn split_valid_utf8(bytes: &[u8]) -> (String, usize) {
match std::str::from_utf8(bytes) {
Ok(s) => (s.to_string(), bytes.len()),
Err(e) => {
let valid_len = e.valid_up_to();
// `error_len() == None` means "incomplete but valid so far" (the
// tail is a truncated multi-byte char — wait for more bytes).
// `Some(_)` means a genuinely invalid byte — replace just that
// one so garbage input can't stall the loop forever.
let consumed = if e.error_len().is_some() { valid_len + 1 } else { valid_len };
(String::from_utf8_lossy(&bytes[..consumed]).into_owned(), consumed)
}
}
}

/// Line-based attach: reads stdin a line at a time and appends `\r`. Used
/// when stdin isn't a real tty (piped input, scripts) and as the whole
/// implementation on non-unix, where raw termios/ioctl aren't available.
async fn attach_fallback(id: &str) -> std::io::Result<()> {
let stream = TcpStream::connect(crate::addr()).await?;
let (read_half, mut write_half) = stream.into_split();
let subscribe = json!({ "id": "1", "cmd": "terminal.subscribe", "pty_id": id }).to_string();
write_half.write_all(subscribe.as_bytes()).await?;
write_half.write_all(b"\n").await?;

tokio::spawn(async move {
let mut lines = BufReader::new(read_half).lines();
let mut stdout = tokio::io::stdout();
while let Ok(Some(line)) = lines.next_line().await {
let Ok(value) = serde_json::from_str::<Value>(&line) else {
continue;
};
match value.get("event").and_then(Value::as_str) {
Some("terminal.output") => {
if let Some(data) = value.get("data").and_then(Value::as_str) {
let _ = stdout.write_all(data.as_bytes()).await;
let _ = stdout.flush().await;
}
}
Some("terminal.exit") => break,
_ => {}
}
}
});

let mut stdin = BufReader::new(tokio::io::stdin()).lines();
while let Some(line) = stdin.next_line().await? {
let write =
json!({ "cmd": "terminal.write", "pty_id": id, "data": format!("{line}\r") }).to_string();
write_half.write_all(write.as_bytes()).await?;
write_half.write_all(b"\n").await?;
}
Ok(())
}

/// Puts fd 0 in raw mode for its lifetime and restores the original termios
/// on drop (including on panic — the workspace doesn't use `panic =
/// "abort"`, so unwinding still runs `Drop`).
#[cfg(unix)]
struct RawModeGuard {
fd: RawFd,
original: libc::termios,
}

#[cfg(unix)]
impl RawModeGuard {
fn enable(fd: RawFd) -> std::io::Result<Self> {
let mut original: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(fd, &mut original) } != 0 {
return Err(std::io::Error::last_os_error());
}
let mut raw = original;
unsafe { libc::cfmakeraw(&mut raw) };
if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Self { fd, original })
}
}

#[cfg(unix)]
impl Drop for RawModeGuard {
fn drop(&mut self) {
unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &self.original) };
}
}

#[cfg(unix)]
fn term_size(fd: RawFd) -> Option<(u16, u16)> {
let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
if unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) } != 0 {
return None;
}
(ws.ws_row > 0 && ws.ws_col > 0).then_some((ws.ws_row, ws.ws_col))
}

#[cfg(unix)]
async fn attach_unix(id: &str) -> std::io::Result<()> {
let stream = TcpStream::connect(crate::addr()).await?;
let (read_half, write_half) = stream.into_split();

// Single writer task: both the stdin loop and the resize watcher just
// send lines here, so two tasks never race on the same socket half
// (matches the daemon's own single-writer pattern in ipc.rs::handle_conn).
let (out_tx, mut out_rx) = mpsc::unbounded_channel::<String>();
let writer = tokio::spawn(async move {
let mut write_half = write_half;
while let Some(line) = out_rx.recv().await {
if write_half.write_all(line.as_bytes()).await.is_err() { break; }
if write_half.write_all(b"\n").await.is_err() { break; }
}
});

let _ = out_tx.send(json!({ "id": "1", "cmd": "terminal.subscribe", "pty_id": id }).to_string());
if let Some((rows, cols)) = term_size(0) {
let _ = out_tx.send(json!({ "cmd": "terminal.resize", "pty_id": id, "rows": rows, "cols": cols }).to_string());
}

let (exit_tx, mut exit_rx) = oneshot::channel::<()>();
tokio::spawn(async move {
let mut lines = BufReader::new(read_half).lines();
let mut stdout = tokio::io::stdout();
while let Ok(Some(line)) = lines.next_line().await {
let Ok(value) = serde_json::from_str::<Value>(&line) else { continue };
match value.get("event").and_then(Value::as_str) {
Some("terminal.output") => {
if let Some(data) = value.get("data").and_then(Value::as_str) {
let _ = stdout.write_all(data.as_bytes()).await;
let _ = stdout.flush().await;
}
}
Some("terminal.exit") => break,
_ => {}
}
}
let _ = exit_tx.send(());
});

let raw_guard = RawModeGuard::enable(0)?;
let mut winch = signal(SignalKind::window_change())?;
let mut term = signal(SignalKind::terminate())?;
let mut hup = signal(SignalKind::hangup())?;
let mut quit = signal(SignalKind::quit())?;

let mut stdin = tokio::io::stdin();
let mut buf = [0u8; 4096];
let mut pending: Vec<u8> = Vec::new();
loop {
tokio::select! {
result = stdin.read(&mut buf) => {
let n = result?;
if n == 0 { break; }
pending.extend_from_slice(&buf[..n]);
let (text, consumed) = split_valid_utf8(&pending);
if !text.is_empty() {
let _ = out_tx.send(json!({ "cmd": "terminal.write", "pty_id": id, "data": text }).to_string());
}
pending.drain(..consumed);
}
_ = winch.recv() => {
if let Some((rows, cols)) = term_size(0) {
let _ = out_tx.send(json!({ "cmd": "terminal.resize", "pty_id": id, "rows": rows, "cols": cols }).to_string());
}
}
_ = term.recv() => break,
_ = hup.recv() => break,
_ = quit.recv() => break,
_ = &mut exit_rx => break,
}
}

drop(raw_guard);
drop(out_tx);
writer.abort();

// `tokio::io::stdin()` reads run on a dedicated blocking OS thread (fd 0
// isn't reliably non-blocking-pollable on unix), and that thread stays
// blocked in its `read()` syscall for as long as nobody types anything
// or closes stdin. The tokio runtime waits for outstanding blocking
// threads on shutdown, so a plain return here would hang the process
// until the next keystroke — even though the attach session is already
// over. Exit immediately instead (same pattern as `daemon.shutdown` in
// ipc.rs); the terminal was already restored by `raw_guard` above.
std::process::exit(0);
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn split_valid_utf8_returns_all_of_a_plain_ascii_chunk() {
let (text, consumed) = split_valid_utf8(b"hola");
assert_eq!(text, "hola");
assert_eq!(consumed, 4);
}

#[test]
fn split_valid_utf8_holds_back_a_multibyte_char_split_across_reads() {
// 'ñ' is 2 bytes (0xC3 0xB1) — simulate a read() that only got the
// first byte of it.
let full = "hola ñ".as_bytes();
let cut = full.len() - 1; // drop the last byte of 'ñ'
let (text, consumed) = split_valid_utf8(&full[..cut]);
assert_eq!(text, "hola ");
assert_eq!(consumed, "hola ".len());

// Feeding the rest (the held-back byte + the missing one) completes it.
let mut rest = full[consumed..cut].to_vec();
rest.push(full[cut]);
let (text2, consumed2) = split_valid_utf8(&rest);
assert_eq!(text2, "ñ");
assert_eq!(consumed2, rest.len());
}

#[test]
fn split_valid_utf8_skips_a_single_invalid_byte_instead_of_stalling() {
let bytes = [b'h', b'i', 0xFF, b'!'];
let (text, consumed) = split_valid_utf8(&bytes);
assert_eq!(text, "hi\u{FFFD}");
assert_eq!(consumed, 3);
}

#[test]
fn split_valid_utf8_returns_empty_for_empty_input() {
let (text, consumed) = split_valid_utf8(b"");
assert_eq!(text, "");
assert_eq!(consumed, 0);
}
}
Loading
Loading