feat: Phase 7 — Accept/Reject DiffView, @-Mentions, Checkpoint/Rewind [appimage]
All checks were successful
Build AppImage / build (push) Successful in 10m19s
All checks were successful
Build AppImage / build (push) Successful in 10m19s
Accept/Reject DiffView: - Bridge sendet checkpoint-before/after Events bei Edit/Write - Rust speichert Datei-Snapshots in SQLite (content_before/after) - Frontend zeigt interaktive DiffView mit Accept/Reject-Buttons - Bei Reject: Datei wird automatisch auf Zustand VOR der Aenderung zurueckgesetzt - Neues Modul: checkpoint.rs mit Accept/Reject/Rewind Commands @-Mentions (Datei-Referenzen): - @datei.ts im Chat-Input oeffnet Fuzzy-Autocomplete - Fuzzy-Suche scannt Projektverzeichnis (max 5000 Dateien) - Score-basiertes Ranking (Anfang, Separator, konsekutive Matches) - Bei Auswahl: Dateiinhalt wird in Prompt injiziert (als Code-Block) - @datei.ts#5-10 fuer Zeilenbereiche - Neue Komponente: FileMention.svelte Checkpoint/Rewind: - Automatische Snapshots bei jeder Dateiänderung (Edit/Write) - SQLite-Tabelle checkpoints mit content_before/content_after - Rewind: Alle Dateien ab einem Checkpoint zuruecksetzen - Commands: accept_change, reject_change, list_checkpoints, rewind_to_checkpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f394d69b70
commit
c779fa7fc5
11 changed files with 1063 additions and 24 deletions
|
|
@ -43,6 +43,10 @@ let isQueryRunning = false;
|
||||||
// So kann der User wie in Claude Code/VS Code Extension weiter tippen.
|
// So kann der User wie in Claude Code/VS Code Extension weiter tippen.
|
||||||
const pendingMessages = [];
|
const pendingMessages = [];
|
||||||
|
|
||||||
|
// Tool-Changes: Bei Edit/Write wird der Diff an Rust/Frontend gemeldet
|
||||||
|
// damit der User Accept/Reject entscheiden kann (Post-Execution mit Rewind)
|
||||||
|
const CHANGE_TOOLS = ['Edit', 'Write'];
|
||||||
|
|
||||||
// Agent-Modus (solo | handlanger | experten | auto)
|
// Agent-Modus (solo | handlanger | experten | auto)
|
||||||
let agentMode = 'solo';
|
let agentMode = 'solo';
|
||||||
|
|
||||||
|
|
@ -500,6 +504,9 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
// als auch als standalone tool_use Event. Via toolUseId deduplizieren.
|
// als auch als standalone tool_use Event. Via toolUseId deduplizieren.
|
||||||
const handledTools = new Set();
|
const handledTools = new Set();
|
||||||
|
|
||||||
|
// Tool-Info Cache: toolId → { name, filePath } fuer Checkpoint-After
|
||||||
|
const toolInfoCache = new Map();
|
||||||
|
|
||||||
// Tool-Use handhaben — funktioniert sowohl fuer standalone tool_use Events
|
// Tool-Use handhaben — funktioniert sowohl fuer standalone tool_use Events
|
||||||
// als auch fuer tool_use Bloecke innerhalb einer assistant-Nachricht
|
// als auch fuer tool_use Bloecke innerhalb einer assistant-Nachricht
|
||||||
function handleToolUse(ev) {
|
function handleToolUse(ev) {
|
||||||
|
|
@ -544,6 +551,19 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
agentId: currentAgentId,
|
agentId: currentAgentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bei Edit/Write: Tool-Info cachen + Checkpoint-Event senden
|
||||||
|
if (CHANGE_TOOLS.includes(toolName) && toolInput.file_path) {
|
||||||
|
toolInfoCache.set(toolId, { name: toolName, filePath: toolInput.file_path });
|
||||||
|
sendEvent('checkpoint-before', {
|
||||||
|
toolId,
|
||||||
|
tool: toolName,
|
||||||
|
filePath: toolInput.file_path,
|
||||||
|
oldString: toolInput.old_string || null, // Edit: was ersetzt wird
|
||||||
|
newString: toolInput.new_string || null, // Edit: womit ersetzt wird
|
||||||
|
content: toolInput.content || null, // Write: neuer Inhalt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const toolSummary = summarizeToolInput(toolName, toolInput);
|
const toolSummary = summarizeToolInput(toolName, toolInput);
|
||||||
sendMonitorEvent('tool', `${toolName} ${toolSummary}`, {
|
sendMonitorEvent('tool', `${toolName} ${toolSummary}`, {
|
||||||
toolId,
|
toolId,
|
||||||
|
|
@ -572,6 +592,17 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
success: !ev.is_error,
|
success: !ev.is_error,
|
||||||
agentId: currentAgentId,
|
agentId: currentAgentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bei Edit/Write: Checkpoint-After senden damit Frontend den Diff anzeigen kann
|
||||||
|
const toolInfo = toolInfoCache.get(toolId);
|
||||||
|
if (!ev.is_error && toolInfo) {
|
||||||
|
sendEvent('checkpoint-after', {
|
||||||
|
toolId,
|
||||||
|
tool: toolInfo.name,
|
||||||
|
filePath: toolInfo.filePath,
|
||||||
|
});
|
||||||
|
toolInfoCache.delete(toolId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilfsfunktion: Iteration mit Fallback bei ungueltiger Session-ID.
|
// Hilfsfunktion: Iteration mit Fallback bei ungueltiger Session-ID.
|
||||||
|
|
|
||||||
125
src-tauri/src/checkpoint.rs
Normal file
125
src-tauri/src/checkpoint.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
// Claude Desktop — Checkpoint-System (Accept/Reject + Rewind)
|
||||||
|
// Ermoeglicht das Rueckgaengigmachen von Datei-Aenderungen
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tauri::command;
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
|
|
||||||
|
/// Checkpoint-Info fuer Frontend (Serialisierbar)
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct CheckpointInfo {
|
||||||
|
pub tool_id: String,
|
||||||
|
pub tool_name: String,
|
||||||
|
pub file_path: String,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub has_diff: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aenderung akzeptieren — Checkpoint als 'accepted' markieren
|
||||||
|
#[command]
|
||||||
|
pub fn accept_change(
|
||||||
|
tool_id: String,
|
||||||
|
db_state: tauri::State<'_, Arc<Mutex<db::Database>>>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let db = db_state.lock().map_err(|e| e.to_string())?;
|
||||||
|
db.accept_checkpoint(&tool_id)
|
||||||
|
.map_err(|e| format!("Checkpoint akzeptieren fehlgeschlagen: {}", e))?;
|
||||||
|
println!("✅ Aenderung akzeptiert: {}", tool_id);
|
||||||
|
Ok("akzeptiert".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aenderung ablehnen — Datei auf content_before zuruecksetzen
|
||||||
|
#[command]
|
||||||
|
pub fn reject_change(
|
||||||
|
tool_id: String,
|
||||||
|
db_state: tauri::State<'_, Arc<Mutex<db::Database>>>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let db = db_state.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let checkpoint = db
|
||||||
|
.get_checkpoint(&tool_id)
|
||||||
|
.map_err(|e| format!("Checkpoint laden fehlgeschlagen: {}", e))?
|
||||||
|
.ok_or_else(|| format!("Checkpoint nicht gefunden: {}", tool_id))?;
|
||||||
|
|
||||||
|
// Datei auf Zustand VOR der Aenderung zuruecksetzen
|
||||||
|
std::fs::write(&checkpoint.file_path, &checkpoint.content_before)
|
||||||
|
.map_err(|e| format!("Datei wiederherstellen fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
db.reject_checkpoint(&tool_id)
|
||||||
|
.map_err(|e| format!("Checkpoint Status-Update fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"↩️ Aenderung abgelehnt + zurueckgesetzt: {} ({})",
|
||||||
|
checkpoint.file_path, tool_id
|
||||||
|
);
|
||||||
|
Ok(format!("Datei zurueckgesetzt: {}", checkpoint.file_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alle Checkpoints einer Session auflisten
|
||||||
|
#[command]
|
||||||
|
pub fn list_checkpoints(
|
||||||
|
session_id: String,
|
||||||
|
db_state: tauri::State<'_, Arc<Mutex<db::Database>>>,
|
||||||
|
) -> Result<Vec<CheckpointInfo>, String> {
|
||||||
|
let db = db_state.lock().map_err(|e| e.to_string())?;
|
||||||
|
let checkpoints = db
|
||||||
|
.list_checkpoints(&session_id)
|
||||||
|
.map_err(|e| format!("Checkpoints laden fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
Ok(checkpoints
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| CheckpointInfo {
|
||||||
|
tool_id: c.tool_id,
|
||||||
|
tool_name: c.tool_name,
|
||||||
|
file_path: c.file_path,
|
||||||
|
status: c.status,
|
||||||
|
created_at: c.created_at,
|
||||||
|
has_diff: c.content_after.is_some(),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zu einem bestimmten Checkpoint zurueckspulen (alle danach auch revert)
|
||||||
|
#[command]
|
||||||
|
pub fn rewind_to_checkpoint(
|
||||||
|
tool_id: String,
|
||||||
|
session_id: String,
|
||||||
|
db_state: tauri::State<'_, Arc<Mutex<db::Database>>>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let db = db_state.lock().map_err(|e| e.to_string())?;
|
||||||
|
let checkpoints = db
|
||||||
|
.list_checkpoints(&session_id)
|
||||||
|
.map_err(|e| format!("Checkpoints laden fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
// Alle Checkpoints ab dem angegebenen zuruecksetzen (neueste zuerst = richtige Reihenfolge)
|
||||||
|
let mut reverted_files = Vec::new();
|
||||||
|
let mut found = false;
|
||||||
|
|
||||||
|
for cp in &checkpoints {
|
||||||
|
if cp.tool_id == tool_id {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
if found && (cp.status == "completed" || cp.status == "pending") {
|
||||||
|
// Datei wiederherstellen
|
||||||
|
if let Err(e) = std::fs::write(&cp.file_path, &cp.content_before) {
|
||||||
|
println!("⚠️ Rewind fehlgeschlagen fuer {}: {}", cp.file_path, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = db.reject_checkpoint(&cp.tool_id);
|
||||||
|
reverted_files.push(cp.file_path.clone());
|
||||||
|
println!("↩️ Rewind: {}", cp.file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return Err(format!("Checkpoint nicht gefunden: {}", tool_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"↩️ Rewind abgeschlossen: {} Dateien zurueckgesetzt",
|
||||||
|
reverted_files.len()
|
||||||
|
);
|
||||||
|
Ok(reverted_files)
|
||||||
|
}
|
||||||
|
|
@ -515,6 +515,57 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) {
|
||||||
let mut state = state.lock().unwrap();
|
let mut state = state.lock().unwrap();
|
||||||
state.agents.clear();
|
state.agents.clear();
|
||||||
}
|
}
|
||||||
|
"checkpoint-before" => {
|
||||||
|
// Datei VOR der Aenderung lesen und als Snapshot speichern
|
||||||
|
if let Some(file_path) = payload.get("filePath").and_then(|v| v.as_str()) {
|
||||||
|
let content_before = std::fs::read_to_string(file_path).unwrap_or_default();
|
||||||
|
let tool_id = payload.get("toolId").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let tool_name = payload.get("tool").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
// In DB speichern
|
||||||
|
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
|
||||||
|
let db_lock = db_state.lock().unwrap();
|
||||||
|
if let Ok(Some(session_id)) = db_lock.get_setting("active_session_id") {
|
||||||
|
let _ = db_lock.save_checkpoint(
|
||||||
|
tool_id,
|
||||||
|
&session_id,
|
||||||
|
tool_name,
|
||||||
|
file_path,
|
||||||
|
&content_before,
|
||||||
|
);
|
||||||
|
println!("📸 Checkpoint: {} ({} Bytes)", file_path, content_before.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = app.emit("checkpoint-before", &payload);
|
||||||
|
}
|
||||||
|
"checkpoint-after" => {
|
||||||
|
// Datei NACH der Aenderung lesen und Diff ans Frontend senden
|
||||||
|
if let Some(file_path) = payload.get("filePath").and_then(|v| v.as_str()) {
|
||||||
|
let tool_id = payload.get("toolId").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let content_after = std::fs::read_to_string(file_path).unwrap_or_default();
|
||||||
|
|
||||||
|
// content_before aus DB holen
|
||||||
|
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
|
||||||
|
let db_lock = db_state.lock().unwrap();
|
||||||
|
if let Ok(Some(checkpoint)) = db_lock.get_checkpoint(tool_id) {
|
||||||
|
// Erweitertes Event mit Diff-Daten ans Frontend
|
||||||
|
let change_event = serde_json::json!({
|
||||||
|
"toolId": tool_id,
|
||||||
|
"tool": payload.get("tool"),
|
||||||
|
"filePath": file_path,
|
||||||
|
"contentBefore": checkpoint.content_before,
|
||||||
|
"contentAfter": content_after,
|
||||||
|
});
|
||||||
|
let _ = app.emit("file-change", &change_event);
|
||||||
|
println!("📝 Datei-Aenderung: {}", file_path);
|
||||||
|
|
||||||
|
// content_after in Checkpoint updaten
|
||||||
|
let _ = db_lock.update_checkpoint_after(tool_id, &content_after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
other => {
|
other => {
|
||||||
// Generische Weiterleitung aller Bridge-Events ans Frontend
|
// Generische Weiterleitung aller Bridge-Events ans Frontend
|
||||||
// (subagent-started, subagent-stopped, monitor-event, mode-changed,
|
// (subagent-started, subagent-stopped, monitor-event, mode-changed,
|
||||||
|
|
|
||||||
|
|
@ -591,3 +591,205 @@ pub async fn list_sticky_context(
|
||||||
|
|
||||||
db.load_sticky_context().map_err(|e| e.to_string())
|
db.load_sticky_context().map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ @-Mentions: Fuzzy File Search ============
|
||||||
|
|
||||||
|
/// Datei-Ergebnis fuer Frontend
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct FileResult {
|
||||||
|
pub name: String,
|
||||||
|
pub path: String, // Relativer Pfad
|
||||||
|
pub full_path: String, // Absoluter Pfad
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ignorierte Verzeichnisse beim Datei-Scan
|
||||||
|
const IGNORE_DIRS: &[&str] = &[
|
||||||
|
"node_modules", ".git", "target", "build", "dist", ".svelte-kit",
|
||||||
|
"__pycache__", ".next", ".nuxt", "vendor", ".cargo",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Relativen Pfad in absoluten aufloesen (relativ zum Projektverzeichnis)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn resolve_file_path(
|
||||||
|
relative_path: String,
|
||||||
|
app: AppHandle,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let working_dir = {
|
||||||
|
let state = app.state::<DbState>();
|
||||||
|
let db = state.lock().unwrap();
|
||||||
|
db.get_setting("working_dir")
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| ".".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let full_path = std::path::Path::new(&working_dir).join(&relative_path);
|
||||||
|
if full_path.exists() {
|
||||||
|
Ok(full_path.to_string_lossy().to_string())
|
||||||
|
} else {
|
||||||
|
Err(format!("Datei nicht gefunden: {}", relative_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dateiinhalt lesen (optional mit Zeilenbereich)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn read_file_content(
|
||||||
|
file_path: String,
|
||||||
|
line_range: Option<String>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let content = std::fs::read_to_string(&file_path)
|
||||||
|
.map_err(|e| format!("Datei lesen fehlgeschlagen: {}", e))?;
|
||||||
|
|
||||||
|
// Zeilenbereich anwenden: "5-10" oder "5"
|
||||||
|
if let Some(range) = line_range {
|
||||||
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
|
let parts: Vec<&str> = range.split('-').collect();
|
||||||
|
let start = parts[0].parse::<usize>().unwrap_or(1).saturating_sub(1);
|
||||||
|
let end = if parts.len() > 1 {
|
||||||
|
parts[1].parse::<usize>().unwrap_or(lines.len())
|
||||||
|
} else {
|
||||||
|
start + 1
|
||||||
|
};
|
||||||
|
let end = end.min(lines.len());
|
||||||
|
|
||||||
|
Ok(lines[start..end].join("\n"))
|
||||||
|
} else {
|
||||||
|
// Max 50KB
|
||||||
|
if content.len() > 50_000 {
|
||||||
|
Ok(content[..50_000].to_string() + "\n... (abgeschnitten bei 50KB)")
|
||||||
|
} else {
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fuzzy-Suche in Projektdateien
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn fuzzy_search_files(
|
||||||
|
query: String,
|
||||||
|
app: AppHandle,
|
||||||
|
) -> Result<Vec<FileResult>, String> {
|
||||||
|
// Aktives Projektverzeichnis ermitteln
|
||||||
|
let working_dir = {
|
||||||
|
let state = app.state::<DbState>();
|
||||||
|
let db = state.lock().unwrap();
|
||||||
|
db.get_setting("working_dir")
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| ".".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
let mut results: Vec<(FileResult, i32)> = Vec::new();
|
||||||
|
|
||||||
|
// Rekursiv Dateien scannen (max 5000)
|
||||||
|
let mut stack = vec![std::path::PathBuf::from(&working_dir)];
|
||||||
|
let mut file_count = 0;
|
||||||
|
let max_files = 5000;
|
||||||
|
|
||||||
|
while let Some(dir) = stack.pop() {
|
||||||
|
if file_count >= max_files {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = match std::fs::read_dir(&dir) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Ignorierte Verzeichnisse ueberspringen
|
||||||
|
if path.is_dir() {
|
||||||
|
if !IGNORE_DIRS.contains(&file_name.as_str()) && !file_name.starts_with('.') {
|
||||||
|
stack.push(path);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_count += 1;
|
||||||
|
|
||||||
|
// Fuzzy-Match: Query-Zeichen muessen in Reihenfolge im Dateinamen vorkommen
|
||||||
|
let name_lower = file_name.to_lowercase();
|
||||||
|
let score = fuzzy_score(&query_lower, &name_lower);
|
||||||
|
|
||||||
|
if score > 0 {
|
||||||
|
let rel_path = path
|
||||||
|
.strip_prefix(&working_dir)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
results.push((
|
||||||
|
FileResult {
|
||||||
|
name: file_name,
|
||||||
|
path: rel_path,
|
||||||
|
full_path: path.to_string_lossy().to_string(),
|
||||||
|
},
|
||||||
|
score,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach Score sortieren (hoechster zuerst), max 10 Ergebnisse
|
||||||
|
results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
Ok(results.into_iter().take(10).map(|(r, _)| r).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fuzzy-Score berechnen: Wie gut passt query in target?
|
||||||
|
/// Hoeher = besser. 0 = kein Match.
|
||||||
|
fn fuzzy_score(query: &str, target: &str) -> i32 {
|
||||||
|
let query_chars: Vec<char> = query.chars().collect();
|
||||||
|
let target_chars: Vec<char> = target.chars().collect();
|
||||||
|
|
||||||
|
if query_chars.is_empty() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut score = 0i32;
|
||||||
|
let mut qi = 0;
|
||||||
|
let mut prev_match = false;
|
||||||
|
|
||||||
|
for (ti, &tc) in target_chars.iter().enumerate() {
|
||||||
|
if qi < query_chars.len() && tc == query_chars[qi] {
|
||||||
|
score += 10; // Basis-Punkte fuer Match
|
||||||
|
|
||||||
|
// Bonus fuer aufeinanderfolgende Matches
|
||||||
|
if prev_match {
|
||||||
|
score += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus fuer Match am Anfang
|
||||||
|
if ti == 0 {
|
||||||
|
score += 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus fuer Match nach Separator (/, -, _, .)
|
||||||
|
if ti > 0 {
|
||||||
|
let prev = target_chars[ti - 1];
|
||||||
|
if prev == '/' || prev == '-' || prev == '_' || prev == '.' {
|
||||||
|
score += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qi += 1;
|
||||||
|
prev_match = true;
|
||||||
|
} else {
|
||||||
|
prev_match = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Query-Zeichen muessen matchen
|
||||||
|
if qi < query_chars.len() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus fuer exakte Laenge-Naehe (kurze Namen bevorzugen)
|
||||||
|
let length_diff = (target_chars.len() as i32 - query_chars.len() as i32).abs();
|
||||||
|
score -= length_diff;
|
||||||
|
|
||||||
|
score.max(1)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,19 @@ pub struct MonitorEvent {
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checkpoint-Eintrag (Datei-Snapshot fuer Accept/Reject + Rewind)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct CheckpointEntry {
|
||||||
|
pub tool_id: String,
|
||||||
|
pub session_id: String,
|
||||||
|
pub tool_name: String,
|
||||||
|
pub file_path: String,
|
||||||
|
pub content_before: String,
|
||||||
|
pub content_after: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Datenbank-Wrapper
|
/// Datenbank-Wrapper
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub(crate) conn: Connection,
|
pub(crate) conn: Connection,
|
||||||
|
|
@ -230,6 +243,19 @@ impl Database {
|
||||||
UNIQUE(error_hash)
|
UNIQUE(error_hash)
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_error_tracker_count ON error_tracker(occurrence_count DESC);
|
CREATE INDEX IF NOT EXISTS idx_error_tracker_count ON error_tracker(occurrence_count DESC);
|
||||||
|
|
||||||
|
-- Checkpoints: Datei-Snapshots fuer Accept/Reject und Rewind
|
||||||
|
CREATE TABLE IF NOT EXISTS checkpoints (
|
||||||
|
tool_id TEXT PRIMARY KEY,
|
||||||
|
session_id TEXT NOT NULL,
|
||||||
|
tool_name TEXT NOT NULL,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
content_before TEXT NOT NULL,
|
||||||
|
content_after TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id, created_at DESC);
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -887,6 +913,94 @@ impl Database {
|
||||||
Ok(counts)
|
Ok(counts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Checkpoints (Accept/Reject + Rewind) ============
|
||||||
|
|
||||||
|
/// Checkpoint speichern (VOR der Aenderung)
|
||||||
|
pub fn save_checkpoint(
|
||||||
|
&self,
|
||||||
|
tool_id: &str,
|
||||||
|
session_id: &str,
|
||||||
|
tool_name: &str,
|
||||||
|
file_path: &str,
|
||||||
|
content_before: &str,
|
||||||
|
) -> SqlResult<()> {
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO checkpoints (tool_id, session_id, tool_name, file_path, content_before, status)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, 'pending')",
|
||||||
|
params![tool_id, session_id, tool_name, file_path, content_before],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checkpoint After-Content updaten
|
||||||
|
pub fn update_checkpoint_after(&self, tool_id: &str, content_after: &str) -> SqlResult<()> {
|
||||||
|
self.conn.execute(
|
||||||
|
"UPDATE checkpoints SET content_after = ?1, status = 'completed' WHERE tool_id = ?2",
|
||||||
|
params![content_after, tool_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checkpoint abrufen
|
||||||
|
pub fn get_checkpoint(&self, tool_id: &str) -> SqlResult<Option<CheckpointEntry>> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
"SELECT tool_id, session_id, tool_name, file_path, content_before, content_after, status, created_at
|
||||||
|
FROM checkpoints WHERE tool_id = ?1"
|
||||||
|
)?;
|
||||||
|
let result: Vec<CheckpointEntry> = stmt.query_map(params![tool_id], |row| {
|
||||||
|
Ok(CheckpointEntry {
|
||||||
|
tool_id: row.get(0)?,
|
||||||
|
session_id: row.get(1)?,
|
||||||
|
tool_name: row.get(2)?,
|
||||||
|
file_path: row.get(3)?,
|
||||||
|
content_before: row.get(4)?,
|
||||||
|
content_after: row.get(5)?,
|
||||||
|
status: row.get(6)?,
|
||||||
|
created_at: row.get(7)?,
|
||||||
|
})
|
||||||
|
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||||
|
Ok(result.into_iter().next())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alle Checkpoints einer Session (neueste zuerst)
|
||||||
|
pub fn list_checkpoints(&self, session_id: &str) -> SqlResult<Vec<CheckpointEntry>> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
"SELECT tool_id, session_id, tool_name, file_path, content_before, content_after, status, created_at
|
||||||
|
FROM checkpoints WHERE session_id = ?1 ORDER BY created_at DESC LIMIT 100"
|
||||||
|
)?;
|
||||||
|
let result = stmt.query_map(params![session_id], |row| {
|
||||||
|
Ok(CheckpointEntry {
|
||||||
|
tool_id: row.get(0)?,
|
||||||
|
session_id: row.get(1)?,
|
||||||
|
tool_name: row.get(2)?,
|
||||||
|
file_path: row.get(3)?,
|
||||||
|
content_before: row.get(4)?,
|
||||||
|
content_after: row.get(5)?,
|
||||||
|
status: row.get(6)?,
|
||||||
|
created_at: row.get(7)?,
|
||||||
|
})
|
||||||
|
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checkpoint-Status auf 'rejected' setzen
|
||||||
|
pub fn reject_checkpoint(&self, tool_id: &str) -> SqlResult<()> {
|
||||||
|
self.conn.execute(
|
||||||
|
"UPDATE checkpoints SET status = 'rejected' WHERE tool_id = ?1",
|
||||||
|
params![tool_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checkpoint-Status auf 'accepted' setzen
|
||||||
|
pub fn accept_checkpoint(&self, tool_id: &str) -> SqlResult<()> {
|
||||||
|
self.conn.execute(
|
||||||
|
"UPDATE checkpoints SET status = 'accepted' WHERE tool_id = ?1",
|
||||||
|
params![tool_id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Phase 2.0: Fehler-Tracking ============
|
// ============ Phase 2.0: Fehler-Tracking ============
|
||||||
|
|
||||||
/// Fehler-Occurrence zählen und zurückgeben
|
/// Fehler-Occurrence zählen und zurückgeben
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use tauri::{
|
||||||
use webkit2gtk::WebViewExt;
|
use webkit2gtk::WebViewExt;
|
||||||
|
|
||||||
mod audit;
|
mod audit;
|
||||||
|
mod checkpoint;
|
||||||
mod claude;
|
mod claude;
|
||||||
mod clipboard;
|
mod clipboard;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
|
@ -150,6 +151,9 @@ pub fn run() {
|
||||||
context::log_context_failure,
|
context::log_context_failure,
|
||||||
context::get_full_context,
|
context::get_full_context,
|
||||||
context::list_sticky_context,
|
context::list_sticky_context,
|
||||||
|
context::fuzzy_search_files,
|
||||||
|
context::resolve_file_path,
|
||||||
|
context::read_file_content,
|
||||||
// Voice-Interface
|
// Voice-Interface
|
||||||
voice::transcribe_audio,
|
voice::transcribe_audio,
|
||||||
voice::text_to_speech,
|
voice::text_to_speech,
|
||||||
|
|
@ -196,6 +200,11 @@ pub fn run() {
|
||||||
clipboard::clipboard_watch_status,
|
clipboard::clipboard_watch_status,
|
||||||
// Slash-Command Registry
|
// Slash-Command Registry
|
||||||
commands::get_slash_commands,
|
commands::get_slash_commands,
|
||||||
|
// Checkpoints (Accept/Reject + Rewind)
|
||||||
|
checkpoint::accept_change,
|
||||||
|
checkpoint::reject_change,
|
||||||
|
checkpoint::list_checkpoints,
|
||||||
|
checkpoint::rewind_to_checkpoint,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { emit, listen } from '@tauri-apps/api/event';
|
import { emit, listen } from '@tauri-apps/api/event';
|
||||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, agentMode, type Message, type QuickAction } from '$lib/stores/app';
|
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, agentMode, pendingChanges, type Message, type QuickAction, type FileChange } from '$lib/stores/app';
|
||||||
import { currentTool, processingPhase } from '$lib/stores/events';
|
import { currentTool, processingPhase } from '$lib/stores/events';
|
||||||
import { marked, type Tokens } from 'marked';
|
import { marked, type Tokens } from 'marked';
|
||||||
import { tick, onDestroy, onMount } from 'svelte';
|
import { tick, onDestroy, onMount } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import CommandPalette from './CommandPalette.svelte';
|
import CommandPalette from './CommandPalette.svelte';
|
||||||
|
import DiffView from './DiffView.svelte';
|
||||||
|
import FileMention from './FileMention.svelte';
|
||||||
import QuickActions from './QuickActions.svelte';
|
import QuickActions from './QuickActions.svelte';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -323,18 +325,45 @@
|
||||||
let commandQuery = $state('');
|
let commandQuery = $state('');
|
||||||
let commandPaletteRef: CommandPalette | undefined = $state(undefined);
|
let commandPaletteRef: CommandPalette | undefined = $state(undefined);
|
||||||
|
|
||||||
// Slash-Command Erkennung im Input
|
// @-Mention Autocomplete State
|
||||||
|
let showFileMention = $state(false);
|
||||||
|
let mentionQuery = $state('');
|
||||||
|
let fileMentionRef: FileMention | undefined = $state(undefined);
|
||||||
|
|
||||||
|
// Slash-Command und @-Mention Erkennung im Input
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const text = $currentInput;
|
const text = $currentInput;
|
||||||
if (text.startsWith('/')) {
|
if (text.startsWith('/')) {
|
||||||
showCommandPalette = true;
|
showCommandPalette = true;
|
||||||
commandQuery = text.slice(1);
|
commandQuery = text.slice(1);
|
||||||
|
showFileMention = false;
|
||||||
} else {
|
} else {
|
||||||
showCommandPalette = false;
|
showCommandPalette = false;
|
||||||
commandQuery = '';
|
commandQuery = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @-Mention: Suche das letzte @ im Text
|
||||||
|
const atMatch = text.match(/@([\w.\-/]+)$/);
|
||||||
|
if (atMatch && atMatch[1].length > 0) {
|
||||||
|
showFileMention = true;
|
||||||
|
mentionQuery = atMatch[1];
|
||||||
|
} else {
|
||||||
|
showFileMention = false;
|
||||||
|
mentionQuery = '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @-Mention: Datei ausgewaehlt → @query durch @dateiname ersetzen
|
||||||
|
function handleFileSelect(file: { name: string; path: string; full_path: string }) {
|
||||||
|
// Ersetze das letzte @query durch @dateiname
|
||||||
|
const text = $currentInput;
|
||||||
|
const atIdx = text.lastIndexOf('@');
|
||||||
|
if (atIdx >= 0) {
|
||||||
|
$currentInput = text.substring(0, atIdx) + `@${file.path} `;
|
||||||
|
}
|
||||||
|
showFileMention = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Command auswählen: Text ersetzen
|
// Command auswählen: Text ersetzen
|
||||||
function handleCommandSelect(cmd: { name: string; description: string; category: string }) {
|
function handleCommandSelect(cmd: { name: string; description: string; category: string }) {
|
||||||
if (!cmd.name) {
|
if (!cmd.name) {
|
||||||
|
|
@ -734,6 +763,29 @@
|
||||||
$queuedMessage = null;
|
$queuedMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept/Reject Datei-Aenderungen
|
||||||
|
async function acceptChange(toolId: string) {
|
||||||
|
try {
|
||||||
|
await invoke('accept_change', { toolId });
|
||||||
|
pendingChanges.update((changes) => changes.filter((c) => c.toolId !== toolId));
|
||||||
|
console.log('✅ Aenderung akzeptiert:', toolId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Accept fehlgeschlagen:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rejectChange(toolId: string) {
|
||||||
|
try {
|
||||||
|
const result = await invoke<string>('reject_change', { toolId });
|
||||||
|
pendingChanges.update((changes) => changes.filter((c) => c.toolId !== toolId));
|
||||||
|
addMessage('system', `↩️ ${result}`);
|
||||||
|
console.log('↩️ Aenderung abgelehnt:', toolId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Reject fehlgeschlagen:', err);
|
||||||
|
addMessage('system', `Fehler beim Zuruecksetzen: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = $currentInput.trim();
|
const text = $currentInput.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
@ -765,6 +817,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @-Mentions aufloesen: @pfad/datei.ts durch Dateiinhalt ersetzen
|
||||||
|
let resolvedText = text;
|
||||||
|
const mentionPattern = /@([\w.\-/]+(?:#\d+(?:-\d+)?)?)/g;
|
||||||
|
const mentions = [...text.matchAll(mentionPattern)];
|
||||||
|
for (const match of mentions) {
|
||||||
|
const ref = match[1];
|
||||||
|
const [filePath, lineRange] = ref.split('#');
|
||||||
|
try {
|
||||||
|
const fullPath = filePath.startsWith('/') ? filePath : await invoke<string>('resolve_file_path', { relativePath: filePath });
|
||||||
|
const content = await invoke<string>('read_file_content', { filePath: fullPath, lineRange: lineRange || null });
|
||||||
|
const ext = filePath.split('.').pop() || '';
|
||||||
|
resolvedText = resolvedText.replace(
|
||||||
|
match[0],
|
||||||
|
`\n\`\`\`${ext} (${filePath})\n${content}\n\`\`\`\n`
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Datei nicht gefunden — @-Mention unverändert lassen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// User-Nachricht sofort im Chat anzeigen + in DB speichern
|
// User-Nachricht sofort im Chat anzeigen + in DB speichern
|
||||||
const msgId = crypto.randomUUID();
|
const msgId = crypto.randomUUID();
|
||||||
const msg: Message = {
|
const msg: Message = {
|
||||||
|
|
@ -785,7 +857,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke('send_message', { message: text });
|
// Aufgeloesten Text an Claude senden (mit eingebetteten Datei-Inhalten)
|
||||||
|
await invoke('send_message', { message: resolvedText });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Senden:', err);
|
console.error('Fehler beim Senden:', err);
|
||||||
addMessage('system', `Fehler: ${err}`);
|
addMessage('system', `Fehler: ${err}`);
|
||||||
|
|
@ -801,6 +874,12 @@
|
||||||
if (handled) return;
|
if (handled) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileMention hat Vorrang bei @-Autocomplete
|
||||||
|
if (showFileMention && fileMentionRef) {
|
||||||
|
const handled = fileMentionRef.handleKey(event);
|
||||||
|
if (handled) return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ctrl+Enter: Immer senden (auch bei mehrzeiligem Text)
|
// Ctrl+Enter: Immer senden (auch bei mehrzeiligem Text)
|
||||||
if (event.key === 'Enter' && event.ctrlKey) {
|
if (event.key === 'Enter' && event.ctrlKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -1166,6 +1245,32 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pending Changes: DiffView mit Accept/Reject -->
|
||||||
|
{#if $pendingChanges.length > 0}
|
||||||
|
<div class="pending-changes">
|
||||||
|
<div class="pending-changes-header">
|
||||||
|
<span>📝 {$pendingChanges.length} Datei-Aenderung{$pendingChanges.length > 1 ? 'en' : ''}</span>
|
||||||
|
<button
|
||||||
|
class="btn-accept-all"
|
||||||
|
onclick={() => $pendingChanges.forEach((c) => acceptChange(c.toolId))}
|
||||||
|
>
|
||||||
|
✅ Alle behalten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#each $pendingChanges as change (change.toolId)}
|
||||||
|
<DiffView
|
||||||
|
oldText={change.contentBefore}
|
||||||
|
newText={change.contentAfter}
|
||||||
|
filename={change.filePath}
|
||||||
|
interactive={true}
|
||||||
|
toolId={change.toolId}
|
||||||
|
onAccept={acceptChange}
|
||||||
|
onReject={rejectChange}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="chat-input">
|
<div class="chat-input">
|
||||||
<CommandPalette
|
<CommandPalette
|
||||||
bind:this={commandPaletteRef}
|
bind:this={commandPaletteRef}
|
||||||
|
|
@ -1173,6 +1278,12 @@
|
||||||
visible={showCommandPalette}
|
visible={showCommandPalette}
|
||||||
onSelect={handleCommandSelect}
|
onSelect={handleCommandSelect}
|
||||||
/>
|
/>
|
||||||
|
<FileMention
|
||||||
|
bind:this={fileMentionRef}
|
||||||
|
query={mentionQuery}
|
||||||
|
visible={showFileMention}
|
||||||
|
onSelect={handleFileSelect}
|
||||||
|
/>
|
||||||
{#if $agentMode && $agentMode !== 'solo'}
|
{#if $agentMode && $agentMode !== 'solo'}
|
||||||
<div class="mode-indicator mode-{$agentMode}">
|
<div class="mode-indicator mode-{$agentMode}">
|
||||||
<span class="mode-icon">
|
<span class="mode-icon">
|
||||||
|
|
@ -1925,6 +2036,41 @@
|
||||||
/* Alte Typing-Animation entfernt — ersetzt durch .activity-indicator */
|
/* Alte Typing-Animation entfernt — ersetzt durch .activity-indicator */
|
||||||
|
|
||||||
/* Input-Bereich */
|
/* Input-Bereich */
|
||||||
|
/* Pending Changes (Accept/Reject DiffView) */
|
||||||
|
.pending-changes {
|
||||||
|
border-top: 2px solid var(--accent);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-changes-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accept-all {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
border: 1px solid var(--success);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--success);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accept-all:hover {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// DiffView — Zeigt Unterschiede zwischen altem und neuem Code
|
// DiffView — Zeigt Unterschiede zwischen altem und neuem Code
|
||||||
// Verwendet für Edit-Tool Ergebnisse
|
// Mit optionalen Accept/Reject-Buttons fuer interaktiven Modus
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
oldText: string;
|
oldText: string;
|
||||||
newText: string;
|
newText: string;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
interactive?: boolean;
|
||||||
|
toolId?: string;
|
||||||
|
onAccept?: (toolId: string) => void;
|
||||||
|
onReject?: (toolId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { oldText, newText, filename = '', language = '' }: Props = $props();
|
let {
|
||||||
|
oldText,
|
||||||
|
newText,
|
||||||
|
filename = '',
|
||||||
|
language = '',
|
||||||
|
interactive = false,
|
||||||
|
toolId = '',
|
||||||
|
onAccept,
|
||||||
|
onReject,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
// Einfache Diff-Berechnung (zeilenbasiert)
|
// Einfache Diff-Berechnung (zeilenbasiert)
|
||||||
interface DiffLine {
|
interface DiffLine {
|
||||||
|
|
@ -31,7 +44,7 @@
|
||||||
|
|
||||||
while (oldIdx < oldLines.length || newIdx < newLines.length) {
|
while (oldIdx < oldLines.length || newIdx < newLines.length) {
|
||||||
if (lcsIdx < lcs.length && oldIdx < oldLines.length && oldLines[oldIdx] === lcs[lcsIdx]) {
|
if (lcsIdx < lcs.length && oldIdx < oldLines.length && oldLines[oldIdx] === lcs[lcsIdx]) {
|
||||||
// Unveränderte Zeile
|
// Unveraenderte Zeile
|
||||||
if (newIdx < newLines.length && newLines[newIdx] === lcs[lcsIdx]) {
|
if (newIdx < newLines.length && newLines[newIdx] === lcs[lcsIdx]) {
|
||||||
result.push({
|
result.push({
|
||||||
type: 'unchanged',
|
type: 'unchanged',
|
||||||
|
|
@ -42,7 +55,7 @@
|
||||||
newIdx++;
|
newIdx++;
|
||||||
lcsIdx++;
|
lcsIdx++;
|
||||||
} else {
|
} else {
|
||||||
// Neue Zeile hinzugefügt
|
// Neue Zeile hinzugefuegt
|
||||||
result.push({
|
result.push({
|
||||||
type: 'added',
|
type: 'added',
|
||||||
lineNo: { old: null, new: newIdx + 1 },
|
lineNo: { old: null, new: newIdx + 1 },
|
||||||
|
|
@ -59,7 +72,7 @@
|
||||||
});
|
});
|
||||||
oldIdx++;
|
oldIdx++;
|
||||||
} else if (newIdx < newLines.length) {
|
} else if (newIdx < newLines.length) {
|
||||||
// Zeile hinzugefügt
|
// Zeile hinzugefuegt
|
||||||
result.push({
|
result.push({
|
||||||
type: 'added',
|
type: 'added',
|
||||||
lineNo: { old: null, new: newIdx + 1 },
|
lineNo: { old: null, new: newIdx + 1 },
|
||||||
|
|
@ -105,7 +118,7 @@
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff berechnen wenn sich Inputs ändern
|
// Diff berechnen wenn sich Inputs aendern
|
||||||
let diffLines = $derived(computeDiff(oldText, newText));
|
let diffLines = $derived(computeDiff(oldText, newText));
|
||||||
|
|
||||||
// Statistiken
|
// Statistiken
|
||||||
|
|
@ -114,24 +127,81 @@
|
||||||
removed: diffLines.filter(l => l.type === 'removed').length,
|
removed: diffLines.filter(l => l.type === 'removed').length,
|
||||||
unchanged: diffLines.filter(l => l.type === 'unchanged').length,
|
unchanged: diffLines.filter(l => l.type === 'unchanged').length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Nur geaenderte Zeilen anzeigen mit Kontext (3 Zeilen davor/danach)
|
||||||
|
let showFullDiff = $state(false);
|
||||||
|
let contextLines = 3;
|
||||||
|
|
||||||
|
function getVisibleLines(): { line: DiffLine; idx: number }[] {
|
||||||
|
if (showFullDiff) return diffLines.map((line, idx) => ({ line, idx }));
|
||||||
|
|
||||||
|
const changedIndices = new Set<number>();
|
||||||
|
diffLines.forEach((line, idx) => {
|
||||||
|
if (line.type !== 'unchanged') {
|
||||||
|
for (let i = Math.max(0, idx - contextLines); i <= Math.min(diffLines.length - 1, idx + contextLines); i++) {
|
||||||
|
changedIndices.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(changedIndices).sort((a, b) => a - b).map(idx => ({ line: diffLines[idx], idx }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleLines = $derived(getVisibleLines());
|
||||||
|
|
||||||
|
function handleAccept() {
|
||||||
|
if (onAccept && toolId) onAccept(toolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReject() {
|
||||||
|
if (onReject && toolId) onReject(toolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dateiname kuerzen fuer Anzeige
|
||||||
|
function shortenPath(path: string): string {
|
||||||
|
const parts = path.split('/');
|
||||||
|
if (parts.length > 3) return `.../${parts.slice(-2).join('/')}`;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="diff-view">
|
<div class="diff-view" class:interactive>
|
||||||
{#if filename}
|
<div class="diff-header">
|
||||||
<div class="diff-header">
|
<div class="diff-header-left">
|
||||||
<span class="filename">{filename}</span>
|
{#if filename}
|
||||||
|
<span class="filename" title={filename}>{shortenPath(filename)}</span>
|
||||||
|
{/if}
|
||||||
{#if language}
|
{#if language}
|
||||||
<span class="language">{language}</span>
|
<span class="language">{language}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="stats">
|
<span class="stats">
|
||||||
<span class="added">+{stats.added}</span>
|
<span class="stat-added">+{stats.added}</span>
|
||||||
<span class="removed">-{stats.removed}</span>
|
<span class="stat-removed">-{stats.removed}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<div class="diff-header-right">
|
||||||
|
{#if diffLines.length > 20}
|
||||||
|
<button class="btn-toggle" onclick={() => showFullDiff = !showFullDiff}>
|
||||||
|
{showFullDiff ? 'Kompakt' : 'Alles zeigen'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if interactive}
|
||||||
|
<button class="btn-accept" onclick={handleAccept} title="Aenderung behalten">
|
||||||
|
✅ Behalten
|
||||||
|
</button>
|
||||||
|
<button class="btn-reject" onclick={handleReject} title="Aenderung rueckgaengig machen">
|
||||||
|
↩️ Zurueck
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="diff-content">
|
<div class="diff-content">
|
||||||
{#each diffLines as line, idx (idx)}
|
{#each visibleLines as { line, idx } (idx)}
|
||||||
|
{@const prevIdx = visibleLines[visibleLines.indexOf({ line, idx }) - 1]?.idx}
|
||||||
|
{#if idx > 0 && prevIdx !== undefined && idx - prevIdx > 1}
|
||||||
|
<div class="diff-separator">⋯</div>
|
||||||
|
{/if}
|
||||||
<div class="diff-line" class:added={line.type === 'added'} class:removed={line.type === 'removed'}>
|
<div class="diff-line" class:added={line.type === 'added'} class:removed={line.type === 'removed'}>
|
||||||
<span class="line-no old">{line.lineNo.old ?? ''}</span>
|
<span class="line-no old">{line.lineNo.old ?? ''}</span>
|
||||||
<span class="line-no new">{line.lineNo.new ?? ''}</span>
|
<span class="line-no new">{line.lineNo.new ?? ''}</span>
|
||||||
|
|
@ -153,18 +223,42 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diff-view.interactive {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent), 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.diff-header {
|
.diff-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-sm);
|
justify-content: space-between;
|
||||||
padding: var(--spacing-xs) var(--spacing-sm);
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filename {
|
.filename {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language {
|
.language {
|
||||||
|
|
@ -176,23 +270,66 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats .added {
|
.stat-added { color: var(--success); }
|
||||||
color: var(--success);
|
.stat-removed { color: var(--error); }
|
||||||
|
|
||||||
|
.btn-toggle {
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats .removed {
|
.btn-toggle:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accept {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
border: 1px solid var(--success);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--success);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accept:hover {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid var(--error);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject:hover {
|
||||||
|
background: var(--error);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-content {
|
.diff-content {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-height: 300px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,4 +376,13 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-left: var(--spacing-xs);
|
padding-left: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diff-separator {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
169
src/lib/components/FileMention.svelte
Normal file
169
src/lib/components/FileMention.svelte
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
<script lang="ts">
|
||||||
|
// FileMention — @-Autocomplete fuer Projekt-Dateien
|
||||||
|
// Trigger: @ gefolgt von mindestens 1 Zeichen im Chat-Input
|
||||||
|
// Fuzzy-Match gegen Dateien im Projektverzeichnis
|
||||||
|
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
interface FileResult {
|
||||||
|
name: string; // Dateiname
|
||||||
|
path: string; // Relativer Pfad
|
||||||
|
full_path: string; // Absoluter Pfad
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query: string;
|
||||||
|
visible: boolean;
|
||||||
|
onSelect: (file: FileResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { query, visible, onSelect }: Props = $props();
|
||||||
|
|
||||||
|
let results: FileResult[] = $state([]);
|
||||||
|
let selectedIndex = $state(0);
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
// Suche triggern wenn Query sich aendert
|
||||||
|
$effect(() => {
|
||||||
|
if (visible && query.length > 0) {
|
||||||
|
searchFiles(query);
|
||||||
|
} else {
|
||||||
|
results = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function searchFiles(q: string) {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
results = await invoke<FileResult[]>('fuzzy_search_files', { query: q });
|
||||||
|
selectedIndex = 0;
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Datei-Suche fehlgeschlagen:', err);
|
||||||
|
results = [];
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard-Navigation (wird von ChatPanel aufgerufen)
|
||||||
|
export function handleKey(event: KeyboardEvent): boolean {
|
||||||
|
if (!visible || results.length === 0) return false;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
selectedIndex = (selectedIndex + 1) % results.length;
|
||||||
|
return true;
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
selectedIndex = (selectedIndex - 1 + results.length) % results.length;
|
||||||
|
return true;
|
||||||
|
case 'Tab':
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault();
|
||||||
|
if (results[selectedIndex]) {
|
||||||
|
onSelect(results[selectedIndex]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension → Icon Mapping
|
||||||
|
function getFileIcon(name: string): string {
|
||||||
|
const ext = name.split('.').pop()?.toLowerCase() || '';
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
rs: '🦀', ts: '📘', js: '📙', svelte: '🟠',
|
||||||
|
css: '🎨', html: '🌐', json: '📋', md: '📝',
|
||||||
|
toml: '⚙️', yml: '📄', yaml: '📄', sql: '🗃️',
|
||||||
|
sh: '🐚', nix: '❄️', py: '🐍', go: '🔵',
|
||||||
|
};
|
||||||
|
return icons[ext] || '📄';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible && (results.length > 0 || loading)}
|
||||||
|
<div class="file-mention">
|
||||||
|
{#if loading && results.length === 0}
|
||||||
|
<div class="mention-loading">Suche...</div>
|
||||||
|
{/if}
|
||||||
|
{#each results.slice(0, 8) as file, idx (file.full_path)}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="mention-item"
|
||||||
|
class:selected={idx === selectedIndex}
|
||||||
|
onclick={() => onSelect(file)}
|
||||||
|
>
|
||||||
|
<span class="mention-icon">{getFileIcon(file.name)}</span>
|
||||||
|
<span class="mention-name">{file.name}</span>
|
||||||
|
<span class="mention-path">{file.path}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.file-mention {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-loading {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item:hover,
|
||||||
|
.mention-item.selected {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item.selected {
|
||||||
|
border-left: 2px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-icon {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-path {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -59,6 +59,18 @@ export interface QuickAction {
|
||||||
invokeArgs?: Record<string, unknown>;
|
invokeArgs?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pending File-Changes (Accept/Reject DiffView)
|
||||||
|
export interface FileChange {
|
||||||
|
toolId: string;
|
||||||
|
tool: string;
|
||||||
|
filePath: string;
|
||||||
|
contentBefore: string;
|
||||||
|
contentAfter: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pendingChanges = writable<FileChange[]>([]);
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
export const agents = writable<Agent[]>([]);
|
export const agents = writable<Agent[]>([]);
|
||||||
export const toolCalls = writable<ToolCall[]>([]);
|
export const toolCalls = writable<ToolCall[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,13 @@ import {
|
||||||
loadMonitorEventsFromDb,
|
loadMonitorEventsFromDb,
|
||||||
activeKnowledgeHints,
|
activeKnowledgeHints,
|
||||||
agentMode,
|
agentMode,
|
||||||
|
pendingChanges,
|
||||||
type Message,
|
type Message,
|
||||||
type Agent,
|
type Agent,
|
||||||
type MonitorEventType,
|
type MonitorEventType,
|
||||||
type KnowledgeHint,
|
type KnowledgeHint,
|
||||||
type AgentMode
|
type AgentMode,
|
||||||
|
type FileChange,
|
||||||
} from './app';
|
} from './app';
|
||||||
|
|
||||||
// Aktuell laufendes Tool (für inline Aktivitätsanzeige)
|
// Aktuell laufendes Tool (für inline Aktivitätsanzeige)
|
||||||
|
|
@ -459,6 +461,38 @@ export async function initEventListeners(): Promise<void> {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// File-Change Events — Accept/Reject DiffView
|
||||||
|
listeners.push(
|
||||||
|
await listen<{
|
||||||
|
toolId: string;
|
||||||
|
tool: string;
|
||||||
|
filePath: string;
|
||||||
|
contentBefore: string;
|
||||||
|
contentAfter: string;
|
||||||
|
}>('file-change', (event) => {
|
||||||
|
const { toolId, tool, filePath, contentBefore, contentAfter } = event.payload;
|
||||||
|
console.log('📝 Datei-Aenderung:', filePath);
|
||||||
|
|
||||||
|
const change: FileChange = {
|
||||||
|
toolId,
|
||||||
|
tool,
|
||||||
|
filePath,
|
||||||
|
contentBefore,
|
||||||
|
contentAfter,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
pendingChanges.update((changes) => [...changes, change]);
|
||||||
|
|
||||||
|
addMonitorEvent('tool', `Datei geaendert: ${filePath.split('/').pop()}`, {
|
||||||
|
toolId,
|
||||||
|
tool,
|
||||||
|
filePath,
|
||||||
|
addedLines: contentAfter.split('\n').length - contentBefore.split('\n').length,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
console.log('✅ Event-Listener initialisiert');
|
console.log('✅ Event-Listener initialisiert');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue