mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 04:36:44 +02:00
Quand la façade tient la conversation (LOKI_CHAT=rust), read, grep, glob, write, edit, bash et ask s'exécutent dans rust/loki-rs/src/tools au lieu de repartir au Go par /internal/v1/tool/exec : mêmes garde-fous dans le même ordre (commande catastrophique, « & » final en mode Code, bac à sable du mode Code, dossier mémoire interdit, lu avant d'écrire), même diff pour l'UI, même nettoyage de la sortie du shell. Le jeton de l'API interne est retiré de l'environnement du shell de l'agent. L'état partagé reste au Go, en un seul exemplaire (sous-agents et tâches le lisent aussi) : POST /internal/v1/tool/context (dossier de travail, dossier mémoire, cible distante), /tool/tracker (lecture notée, contrôle avant écriture), /tool/wrote (note après écriture, diagnostics LSP). Repli sur le Go pour une cible distante ou un contexte indisponible. Parité : internal/loki/parity_tools_test.go passe 58 appels à execTool sur une arborescence de test et range le journal dans tests/parity_tools ; tests/tools.rs le retrouve à l'identique avec exec_local. Écarts assumés consignés dans docs/superpowers/specs/2026-10-15-rust-migration-lot7-outils-design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
189 lines
5.6 KiB
Rust
189 lines
5.6 KiB
Rust
//! Petit diff ligne à ligne pour l'interface — port de `chat_diff.go`.
|
|
//!
|
|
//! Quand l'agent modifie un fichier (`edit`) ou en crée un (`write`), l'UI
|
|
//! affiche les lignes ajoutées (vert) et retirées (rouge). LCS classique sur
|
|
//! les lignes, avec des garde-fous pour ne jamais transformer un gros
|
|
//! remplacement en pavé. Les compteurs +N / -N sont calculés AVANT la coupe
|
|
//! des lignes affichées.
|
|
|
|
use crate::chat::types::DiffLine;
|
|
|
|
/// Au-delà (partie modifiée), comparaison sans détail (LCS trop lente).
|
|
const DIFF_MAX_LINES: usize = 400;
|
|
/// Lignes envoyées à l'UI (le reste est résumé).
|
|
const DIFF_MAX_SHOWN: usize = 120;
|
|
/// Lignes identiques gardées autour d'un changement.
|
|
const DIFF_CONTEXT: usize = 3;
|
|
const DIFF_OMIT_PREFIX: &str = "…";
|
|
|
|
fn line(op: &str, text: &str) -> DiffLine {
|
|
DiffLine {
|
|
op: op.into(),
|
|
text: text.into(),
|
|
}
|
|
}
|
|
|
|
/// `lineDiff` : lignes à afficher (bornées), lignes ajoutées, retirées.
|
|
pub fn line_diff(old_text: &str, new_text: &str) -> (Vec<DiffLine>, i64, i64) {
|
|
let a = split_lines(old_text);
|
|
let b = split_lines(new_text);
|
|
let mut pre = 0;
|
|
while pre < a.len() && pre < b.len() && a[pre] == b[pre] {
|
|
pre += 1;
|
|
}
|
|
let mut suf = 0;
|
|
while suf < a.len() - pre && suf < b.len() - pre && a[a.len() - 1 - suf] == b[b.len() - 1 - suf]
|
|
{
|
|
suf += 1;
|
|
}
|
|
let (ma, mb) = (&a[pre..a.len() - suf], &b[pre..b.len() - suf]);
|
|
let mid: Vec<DiffLine> = if ma.len() > DIFF_MAX_LINES || mb.len() > DIFF_MAX_LINES {
|
|
ma.iter()
|
|
.map(|l| line("-", l))
|
|
.chain(mb.iter().map(|l| line("+", l)))
|
|
.collect()
|
|
} else {
|
|
lcs_diff(ma, mb)
|
|
};
|
|
let added = mid.iter().filter(|d| d.op == "+").count() as i64;
|
|
let removed = mid.iter().filter(|d| d.op == "-").count() as i64;
|
|
|
|
let mut out = Vec::new();
|
|
if pre > DIFF_CONTEXT {
|
|
out.push(line(" ", &omitted(pre - DIFF_CONTEXT)));
|
|
}
|
|
for l in &a[pre.saturating_sub(DIFF_CONTEXT)..pre] {
|
|
out.push(line(" ", l));
|
|
}
|
|
out.extend(mid);
|
|
for l in &a[a.len() - suf..a.len() - suf + suf.min(DIFF_CONTEXT)] {
|
|
out.push(line(" ", l));
|
|
}
|
|
if suf > DIFF_CONTEXT {
|
|
out.push(line(" ", &omitted(suf - DIFF_CONTEXT)));
|
|
}
|
|
(cap_lines(out), added, removed)
|
|
}
|
|
|
|
/// Diff exact par plus longue sous-séquence commune.
|
|
fn lcs_diff(a: &[String], b: &[String]) -> Vec<DiffLine> {
|
|
let (n, m) = (a.len(), b.len());
|
|
let mut lcs = vec![vec![0usize; m + 1]; n + 1];
|
|
for i in (0..n).rev() {
|
|
for j in (0..m).rev() {
|
|
lcs[i][j] = if a[i] == b[j] {
|
|
lcs[i + 1][j + 1] + 1
|
|
} else {
|
|
lcs[i + 1][j].max(lcs[i][j + 1])
|
|
};
|
|
}
|
|
}
|
|
let mut out = Vec::new();
|
|
let (mut i, mut j) = (0, 0);
|
|
while i < n && j < m {
|
|
if a[i] == b[j] {
|
|
out.push(line(" ", &a[i]));
|
|
i += 1;
|
|
j += 1;
|
|
} else if lcs[i + 1][j] >= lcs[i][j + 1] {
|
|
out.push(line("-", &a[i]));
|
|
i += 1;
|
|
} else {
|
|
out.push(line("+", &b[j]));
|
|
j += 1;
|
|
}
|
|
}
|
|
out.extend(a[i..].iter().map(|l| line("-", l)));
|
|
out.extend(b[j..].iter().map(|l| line("+", l)));
|
|
out
|
|
}
|
|
|
|
/// `addedDiff` : contenu entièrement nouveau, toutes ses lignes en ajout.
|
|
pub fn added_diff(text: &str) -> (Vec<DiffLine>, i64) {
|
|
let ls = split_lines(text);
|
|
let n = ls.len() as i64;
|
|
(cap_lines(ls.iter().map(|l| line("+", l)).collect()), n)
|
|
}
|
|
|
|
/// Un saut de ligne final termine la dernière ligne, il n'en ouvre pas une
|
|
/// vide. Les fins CRLF comptent comme LF.
|
|
fn split_lines(s: &str) -> Vec<String> {
|
|
let s = s.replace("\r\n", "\n");
|
|
let s = s.strip_suffix('\n').unwrap_or(&s);
|
|
if s.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
s.split('\n').map(str::to_string).collect()
|
|
}
|
|
|
|
fn omitted(n: usize) -> String {
|
|
if n == 1 {
|
|
format!("{DIFF_OMIT_PREFIX}(1 ligne identique)")
|
|
} else {
|
|
format!("{DIFF_OMIT_PREFIX}({n} lignes identiques)")
|
|
}
|
|
}
|
|
|
|
fn cap_lines(mut d: Vec<DiffLine>) -> Vec<DiffLine> {
|
|
if d.len() <= DIFF_MAX_SHOWN {
|
|
return d;
|
|
}
|
|
let cut = d.len() - DIFF_MAX_SHOWN;
|
|
d.truncate(DIFF_MAX_SHOWN);
|
|
d.push(line(
|
|
" ",
|
|
&format!("{DIFF_OMIT_PREFIX}({cut} lignes de plus)"),
|
|
));
|
|
d
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn ops(d: &[DiffLine]) -> Vec<String> {
|
|
d.iter().map(|l| format!("{}{}", l.op, l.text)).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn une_ligne_changee_dans_un_gros_bloc() {
|
|
let old: String = (1..=20).map(|i| format!("l{i}\n")).collect();
|
|
let new = old.replace("l10\n", "dix\n");
|
|
let (d, a, r) = line_diff(&old, &new);
|
|
assert_eq!((a, r), (1, 1));
|
|
assert_eq!(
|
|
ops(&d),
|
|
[
|
|
" …(6 lignes identiques)",
|
|
" l7",
|
|
" l8",
|
|
" l9",
|
|
"-l10",
|
|
"+dix",
|
|
" l11",
|
|
" l12",
|
|
" l13",
|
|
" …(7 lignes identiques)"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ajout_et_coupe() {
|
|
let text: String = (0..130).map(|i| format!("{i}\n")).collect();
|
|
let (d, n) = added_diff(&text);
|
|
assert_eq!(n, 130);
|
|
assert_eq!(d.len(), 121);
|
|
assert_eq!(d[120].text, "…(10 lignes de plus)");
|
|
assert_eq!(added_diff("").1, 0);
|
|
assert_eq!(added_diff("a\r\nb\r\n").1, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn gros_remplacement_sans_lcs() {
|
|
let old: String = (0..401).map(|i| format!("a{i}\n")).collect();
|
|
let (_, a, r) = line_diff(&old, "b\n");
|
|
assert_eq!((a, r), (1, 401));
|
|
}
|
|
}
|