Skip to content
Open
147 changes: 147 additions & 0 deletions src-tauri/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ fn current_branch(path: &str) -> Result<String, String> {
Ok(branch)
}

#[tauri::command]
pub async fn git_current_branch(path: String) -> Result<String, String> {
tauri::async_runtime::spawn_blocking(move || current_branch(&path))
.await
.map_err(|e| e.to_string())?
}

Comment on lines +306 to +312

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jelllo

fn backup_ref_for(path: &str) -> Result<String, String> {
Ok(format!("refs/bento/backups/{}", current_branch(path)?))
}
Expand Down Expand Up @@ -536,6 +543,27 @@ pub async fn git_remote_branches(repo: String) -> Result<Vec<String>, String> {
.map_err(|e| e.to_string())?
}

// Lists branches from ALL remotes with full remote/branch format (e.g. "daimoxd/feat/foo").
#[tauri::command]
pub async fn git_all_remote_branches(repo: String) -> Result<Vec<String>, String> {
tauri::async_runtime::spawn_blocking(move || {
if !is_git_repo(&repo) {
return Err("not a git repository".into());
}
let raw = git_output(
&repo,
&["for-each-ref", "--format=%(refname:short)", "refs/remotes"],
)?;
Ok(raw
.lines()
.filter(|line| !line.ends_with("/HEAD") && is_safe_branch(line))
.map(str::to_string)
.collect())
})
.await
.map_err(|e| e.to_string())?
}

#[tauri::command]
pub async fn git_worktree_add(
repo: String,
Expand Down Expand Up @@ -667,6 +695,27 @@ pub async fn git_diff(path: String) -> Result<String, String> {
.map_err(|e| e.to_string())?
}

// Accumulated diff of all commits on the current branch vs <base> (three-dot range).
#[tauri::command]
pub async fn git_branch_diff(path: String, base: String) -> Result<String, String> {
if !is_safe_branch(&base) {
return Err(format!("unsafe base branch: {base}"));
}
tauri::async_runtime::spawn_blocking(move || {
if !is_git_repo(&path) {
return Err("not a git repository".into());
}
// Try local ref first, fall back to origin/ prefix for remote-only branches
let result = git_output(&path, &["diff", &format!("{base}...HEAD")]);
if result.is_ok() {
return result;
}
git_output(&path, &["diff", &format!("origin/{base}...HEAD")])
})
.await
.map_err(|e| e.to_string())?
}

// Validates that a commit message is non-empty (trust boundary: frontend input).
fn is_valid_message(msg: &str) -> bool {
!msg.trim().is_empty()
Expand Down Expand Up @@ -949,6 +998,104 @@ pub async fn git_pr_status(path: String) -> Result<Option<PrStatus>, String> {
.map_err(|e| e.to_string())?
}

// Diff between any two git refs (e.g. "origin/main" vs "origin/feat/foo").
#[tauri::command]
pub async fn git_ref_diff(path: String, base: String, target: String) -> Result<String, String> {
if !is_safe_branch(&base) { return Err(format!("unsafe base: {base}")); }
if !is_safe_branch(&target) { return Err(format!("unsafe target: {target}")); }
tauri::async_runtime::spawn_blocking(move || {
if !is_git_repo(&path) { return Err("not a git repository".into()); }
git_output(&path, &["diff", &format!("{base}...{target}")])
})
.await
.map_err(|e| e.to_string())?
}
Comment on lines +1002 to +1012

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

este bloque

@R0MANDEV R0MANDEV Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hola xdd soy otro tests desde otra cuenta xd


// Returns basic PR info (number, title, url) for any branch via gh CLI.
#[tauri::command]
pub async fn gh_pr_view_branch(path: String, branch: String) -> Result<Option<serde_json::Value>, String> {
tauri::async_runtime::spawn_blocking(move || {
let out = Command::new("gh")
.current_dir(&path)
.args(["pr", "view", &branch, "--json", "number,title,url"])
.output();
let Ok(out) = out else { return Ok(None); };
if !out.status.success() || out.stdout.is_empty() { return Ok(None); }
serde_json::from_slice::<serde_json::Value>(&out.stdout).map(Some).map_err(|e| e.to_string())
})
.await
.map_err(|e| e.to_string())?
}

// Posts a comment on the PR and returns its URL.
#[tauri::command]
pub async fn gh_pr_comment(path: String, branch: String, body: String) -> Result<String, String> {
tauri::async_runtime::spawn_blocking(move || {
let out = Command::new("gh")
.current_dir(&path)
.args(["pr", "comment", &branch, "--body", &body, "--json", "url", "--jq", ".url"])
.output()
.map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
})
.await
.map_err(|e| e.to_string())?
}

// Resolves a git ref to its full SHA.
#[tauri::command]
pub async fn git_rev_parse(path: String, reference: String) -> Result<String, String> {
tauri::async_runtime::spawn_blocking(move || {
git_output(&path, &["rev-parse", &reference]).map(|s| s.trim().to_string())
})
.await
.map_err(|e| e.to_string())?
}

// Posts an inline review comment on a specific file+line via gh api.
#[tauri::command]
pub async fn gh_pr_inline_comment(
path: String,
pr_number: u64,
commit_id: String,
file: String,
line: u64,
start_line: Option<u64>,
body: String,
) -> Result<String, String> {
tauri::async_runtime::spawn_blocking(move || {
let endpoint = format!("repos/{{owner}}/{{repo}}/pulls/{pr_number}/comments");
let mut args = vec![
"api".to_string(), endpoint,
"-f".to_string(), format!("body={body}"),
"-f".to_string(), format!("commit_id={commit_id}"),
"-f".to_string(), format!("path={file}"),
"-F".to_string(), format!("line={line}"),
"-f".to_string(), "side=RIGHT".to_string(),
];
if let Some(sl) = start_line {
if sl < line {
args.extend(["-F".to_string(), format!("start_line={sl}"), "-f".to_string(), "start_side=RIGHT".to_string()]);
}
}
args.extend(["--jq".to_string(), ".html_url".to_string()]);
let out = Command::new("gh")
.current_dir(&path)
.args(&args)
.output()
.map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
})
.await
.map_err(|e| e.to_string())?
}

#[tauri::command]
pub async fn git_push(path: String, force_with_lease: Option<bool>) -> Result<String, String> {
tauri::async_runtime::spawn_blocking(move || {
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,18 @@ fn main() {
git::git_rewrite_preflight,
git::git_default_branch,
git::git_remote_branches,
git::git_all_remote_branches,
git::git_current_branch,
git::git_ref_diff,
git::git_rev_parse,
git::gh_pr_view_branch,
git::gh_pr_comment,
git::gh_pr_inline_comment,
git::git_worktree_add,
git::git_worktree_remove,
git::git_sync,
git::git_diff,
git::git_branch_diff,
git::git_commit,
git::git_fixup,
git::git_push,
Expand Down
2 changes: 2 additions & 0 deletions src/app/createSessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ export function createSessionManager(panels: PanelRegistry, stateRepo: Workspace
{ id: 'new-docker', label: appT('newDocker'), keywords: ['docker', 'contenedores', 'containers', 'logs'], run: () => active?.addPanel('docker') },
{ id: 'new-tasks', label: appT('newTasks'), keywords: ['tareas', 'tasks', 'worktree', 'git', 'paralelo'], run: () => active?.addPanel('tasks') },
{ id: 'new-memory', label: appT('newMemory'), keywords: ['memoria', 'memory', 'contexto', 'decisiones', 'resumen'], run: () => active?.addPanel('memory') },
{ id: 'new-diff', label: appT('newDiff'), keywords: ['diff', 'git', 'cambios', 'changes', 'hunk', 'patch'], run: () => active?.addPanel('diff') },
{ id: 'new-review', label: appT('newReview'), keywords: ['review', 'tech review', 'revisar', 'ia', 'ai', 'agente', 'cambios'], run: () => active?.addPanel('review') },
{
id: 'bind-project', label: appT('bindProject'),
keywords: ['proyecto', 'project', 'carpeta', 'cwd', 'directorio'],
Expand Down
46 changes: 45 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,51 @@
"commandPalette": "Command palette",
"commandPlaceholder": "Type a command…",
"addPanel": "Add panel",
"emptyWorkspace": "Empty workspace"
"emptyWorkspace": "Empty workspace",
"panelDiff": "Diff",
"newDiff": "New Diff panel",
"panelReview": "Tech Review",
"newReview": "New Tech Review panel"
},
"review": {
"title": "Tech Review",
"openRepo": "Open repo…",
"noRepo": "No repository selected",
"noRepoHint": "Open a folder to review its changes",
"noChanges": "No changes to review",
"loading": "Loading…",
"refresh": "Refresh",
"autoRefresh": "Auto-refresh",
"commitMessage": "Commit message…",
"commit": "Commit",
"commitAll": "Commit all",
"discard": "Discard selected",
"discardAll": "Discard all changes",
"discardConfirm": "Discard all uncommitted changes? This cannot be undone.",
"committed": "Committed",
"commitError": "Commit failed",
"files": "{count} file(s) changed",
"noSelection": "Select files to commit",
"modeWorktree": "Working tree",
"modeBranch": "Branch diff",
"baseBranch": "Base:",
"currentBranch": "on {branch}",
"noBranchChanges": "No changes vs {base}",
"selectBranch": "Select a branch on the left to review",
"commentPlaceholder": "Leave a review comment on the PR…",
"sendComment": "Send comment",
"commentSent": "Comment sent"
},
"diff": {
"worktreeMode": "Worktree",
"logMode": "Log",
"noRepo": "No repository selected",
"noRepoHint": "Open a folder to view its changes",
"openRepo": "Open repo…",
"noChanges": "No changes in the working tree",
"loading": "Loading…",
"noFiles": "No files",
"refresh": "Refresh"
},
"tasks": {
"tasks": "Tasks",
Expand Down
46 changes: 45 additions & 1 deletion src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,51 @@
"commandPalette": "Paleta de comandos",
"commandPlaceholder": "Escribe un comando…",
"addPanel": "Añadir panel",
"emptyWorkspace": "Espacio vacío"
"emptyWorkspace": "Espacio vacío",
"panelDiff": "Diff",
"newDiff": "Nuevo panel Diff",
"panelReview": "Tech Review",
"newReview": "Nuevo panel Tech Review"
},
"review": {
"title": "Tech Review",
"openRepo": "Abrir repo…",
"noRepo": "No hay repositorio seleccionado",
"noRepoHint": "Abre una carpeta para revisar sus cambios",
"noChanges": "No hay cambios para revisar",
"loading": "Cargando…",
"refresh": "Actualizar",
"autoRefresh": "Auto-actualizar",
"commitMessage": "Mensaje de commit…",
"commit": "Commit",
"commitAll": "Commit todo",
"discard": "Descartar selección",
"discardAll": "Descartar todos los cambios",
"discardConfirm": "¿Descartar todos los cambios no commiteados? Esta acción no se puede deshacer.",
"committed": "Commit creado",
"commitError": "Error al crear commit",
"files": "{count} archivo(s) cambiado(s)",
"noSelection": "Selecciona archivos para commitear",
"modeWorktree": "Árbol de trabajo",
"modeBranch": "Diff de rama",
"baseBranch": "Base:",
"currentBranch": "en {branch}",
"noBranchChanges": "Sin cambios vs {base}",
"selectBranch": "Selecciona una rama a la izquierda para revisar",
"commentPlaceholder": "Deja un comentario de revisión en el PR…",
"sendComment": "Enviar comentario",
"commentSent": "Comentario enviado"
},
"diff": {
"worktreeMode": "Worktree",
"logMode": "Historial",
"noRepo": "No hay repositorio seleccionado",
"noRepoHint": "Abre una carpeta para ver sus cambios",
"openRepo": "Abrir repo…",
"noChanges": "Sin cambios en el árbol de trabajo",
"loading": "Cargando…",
"noFiles": "Sin archivos",
"refresh": "Actualizar"
},
"tasks": {
"tasks": "Tareas",
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import esCatalog from './es.json'
import enCatalog from './en.json'

export type AppLocale = 'es' | 'en'
export type CatalogNamespace = 'app' | 'tasks'
export type CatalogNamespace = 'app' | 'tasks' | 'diff' | 'review'
export type TranslationValues = Record<string, string | number>
export type AppMessageKey = keyof typeof esCatalog.app
type PanelCatalog = typeof esCatalog.panels
Expand Down
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { dockerPanelDefinition } from './panels/docker/definition'
import { vaultPanelDefinition } from './panels/vault/definition'
import { tasksPanelDefinition } from './panels/tasks/definition'
import { memoryPanelDefinition } from './panels/memory/definition'
import { diffPanelDefinition } from './panels/diff/definition'
import { reviewPanelDefinition } from './panels/review/definition'
import { M3UChannelRepository } from './adapters/M3UChannelRepository'
import { IptvOrgChannelRepository } from './adapters/IptvOrgChannelRepository'
import { LocalStorageFavoritesRepository } from './adapters/LocalStorageFavoritesRepository'
Expand Down Expand Up @@ -80,6 +82,8 @@ panels.register(dockerPanelDefinition)
panels.register(vaultPanelDefinition)
panels.register(tasksPanelDefinition)
panels.register(memoryPanelDefinition(memoryRepo))
panels.register(diffPanelDefinition)
panels.register(reviewPanelDefinition)

const app = document.getElementById('app')!
app.appendChild(createSessionManager(panels, stateRepo))
Expand Down
Loading