Compare commits
No commits in common. "c779fa7fc5666d142e53aa4962113d4d973ea684" and "61541098d7a14893fc19d413e7098848518b0c14" have entirely different histories.
c779fa7fc5
...
61541098d7
11 changed files with 90 additions and 1120 deletions
|
|
@ -36,16 +36,6 @@ if (!IS_DAEMON) process.stdin.resume();
|
||||||
let activeAbort = null;
|
let activeAbort = null;
|
||||||
let currentAgentId = null;
|
let currentAgentId = null;
|
||||||
let currentModel = process.env.CLAUDE_MODEL || 'opus';
|
let currentModel = process.env.CLAUDE_MODEL || 'opus';
|
||||||
let isQueryRunning = false;
|
|
||||||
|
|
||||||
// Pending-Queue: Nachrichten die während einer laufenden query() eingehen
|
|
||||||
// werden hier gepuffert und nach dem aktuellen Turn automatisch abgearbeitet.
|
|
||||||
// So kann der User wie in Claude Code/VS Code Extension weiter tippen.
|
|
||||||
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';
|
||||||
|
|
@ -349,20 +339,6 @@ function summarizeToolInput(tool, input) {
|
||||||
// ============ Claude Agent SDK ============
|
// ============ Claude Agent SDK ============
|
||||||
|
|
||||||
async function sendMessage(message, requestId, model = null, contextOverride = null, resumeSessionId = null) {
|
async function sendMessage(message, requestId, model = null, contextOverride = null, resumeSessionId = null) {
|
||||||
// Wenn bereits eine query() läuft: Nachricht in Pending-Queue puffern
|
|
||||||
// und sofort bestätigen. Wird nach aktuellem Turn automatisch abgearbeitet.
|
|
||||||
if (isQueryRunning) {
|
|
||||||
pendingMessages.push({ message, requestId, model, contextOverride, resumeSessionId });
|
|
||||||
sendResponse(requestId, { status: 'queued', position: pendingMessages.length });
|
|
||||||
sendMonitorEvent('agent', `Nachricht gepuffert (Position ${pendingMessages.length})`, {
|
|
||||||
messageLength: message.length,
|
|
||||||
queueSize: pendingMessages.length,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isQueryRunning = true;
|
|
||||||
|
|
||||||
// Modell für diese Anfrage (Parameter > State > Default)
|
// Modell für diese Anfrage (Parameter > State > Default)
|
||||||
const useModel = model || currentModel;
|
const useModel = model || currentModel;
|
||||||
|
|
||||||
|
|
@ -504,9 +480,6 @@ 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) {
|
||||||
|
|
@ -551,19 +524,6 @@ 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,
|
||||||
|
|
@ -592,17 +552,6 @@ 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.
|
||||||
|
|
@ -818,19 +767,6 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
sendEvent('all-stopped');
|
sendEvent('all-stopped');
|
||||||
currentAgentId = null;
|
currentAgentId = null;
|
||||||
activeAbort = null;
|
activeAbort = null;
|
||||||
isQueryRunning = false;
|
|
||||||
|
|
||||||
// Pending-Queue: Nächste Nachricht automatisch abarbeiten (FIFO)
|
|
||||||
if (pendingMessages.length > 0) {
|
|
||||||
const next = pendingMessages.shift();
|
|
||||||
sendMonitorEvent('agent', `Pending-Nachricht wird verarbeitet (${pendingMessages.length} verbleibend)`, {
|
|
||||||
messageLength: next.message.length,
|
|
||||||
remaining: pendingMessages.length,
|
|
||||||
});
|
|
||||||
// Asynchron starten — nicht awaiten damit finally sauber abschließt
|
|
||||||
sendMessage(next.message, next.requestId, next.model, next.contextOverride, next.resumeSessionId)
|
|
||||||
.catch(err => sendMonitorEvent('error', `Pending-Dispatch fehlgeschlagen: ${err.message}`, {}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
// 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,57 +515,6 @@ 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,205 +591,3 @@ 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,19 +54,6 @@ 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,
|
||||||
|
|
@ -243,19 +230,6 @@ 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(())
|
||||||
|
|
@ -913,94 +887,6 @@ 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,7 +12,6 @@ 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;
|
||||||
|
|
@ -151,9 +150,6 @@ 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,
|
||||||
|
|
@ -200,11 +196,6 @@ 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,14 +1,12 @@
|
||||||
<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, pendingChanges, type Message, type QuickAction, type FileChange } from '$lib/stores/app';
|
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, agentMode, type Message, type QuickAction } 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
|
||||||
|
|
@ -325,45 +323,18 @@
|
||||||
let commandQuery = $state('');
|
let commandQuery = $state('');
|
||||||
let commandPaletteRef: CommandPalette | undefined = $state(undefined);
|
let commandPaletteRef: CommandPalette | undefined = $state(undefined);
|
||||||
|
|
||||||
// @-Mention Autocomplete State
|
// Slash-Command Erkennung im Input
|
||||||
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) {
|
||||||
|
|
@ -746,8 +717,21 @@
|
||||||
addMessage('system', `📋 Clipboard (${content_type}): ${suggestion}\n\`\`\`\n${preview}\n\`\`\``);
|
addMessage('system', `📋 Clipboard (${content_type}): ${suggestion}\n\`\`\`\n${preview}\n\`\`\``);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Legacy-Queue Subscriber (Sicherheitsnetz — Hauptlogik ist jetzt in der Bridge)
|
// Queue-Auto-Dispatch: sobald isProcessing von true auf false wechselt
|
||||||
const unsubProcessing = isProcessing.subscribe(() => {});
|
// wird die naechste Nachricht aus der Queue abgeschickt (FIFO).
|
||||||
|
let lastProcessing = false;
|
||||||
|
const unsubProcessing = isProcessing.subscribe((val) => {
|
||||||
|
if (lastProcessing && !val) {
|
||||||
|
const queue = get(messageQueue);
|
||||||
|
if (queue.length > 0) {
|
||||||
|
const [next, ...rest] = queue;
|
||||||
|
messageQueue.set(rest);
|
||||||
|
$queuedMessage = rest.length > 0 ? rest[0] : null;
|
||||||
|
dispatchMessage(next).catch((e) => console.error('Queue-Dispatch fehlgeschlagen:', e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastProcessing = val;
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleGlobalKeydown);
|
window.removeEventListener('keydown', handleGlobalKeydown);
|
||||||
|
|
@ -758,46 +742,39 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function cancelQueued() {
|
function cancelQueued() {
|
||||||
// Legacy: Queue-Cancel (Bridge hat jetzt eigene Pending-Queue)
|
// Alle gequeuten Nachrichten verwerfen + aus dem Chat entfernen
|
||||||
messageQueue.set([]);
|
messageQueue.set([]);
|
||||||
$queuedMessage = null;
|
$queuedMessage = null;
|
||||||
}
|
messages.update((msgs) => msgs.filter((m) => !(m as any).queued));
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
// Nachricht IMMER sofort senden — auch während Claude arbeitet.
|
// Waehrend Claude antwortet: Nachricht in FIFO-Queue.
|
||||||
// Die Bridge puffert die Nachricht intern und verarbeitet sie
|
// Sofort als User-Message im Chat anzeigen (mit queued-Marker).
|
||||||
// automatisch nach dem aktuellen Turn (wie in Claude Code/VS Code Extension).
|
// Der Subscriber dispatcht automatisch wenn Claude fertig ist.
|
||||||
|
if ($isProcessing) {
|
||||||
|
messageQueue.update((q) => [...q, text]);
|
||||||
|
$queuedMessage = text;
|
||||||
|
// Sofort im Chat anzeigen damit User sieht dass die Nachricht angekommen ist
|
||||||
|
const queuedMsg: Message = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
role: 'user',
|
||||||
|
content: text,
|
||||||
|
timestamp: new Date(),
|
||||||
|
queued: true,
|
||||||
|
};
|
||||||
|
messages.update((msgs) => [...msgs, queuedMsg]);
|
||||||
|
$currentInput = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await dispatchMessage(text);
|
await dispatchMessage(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Den eigentlichen Send-Flow — sendet immer sofort an die Bridge.
|
// Den eigentlichen Send-Flow ausgelagert, damit er auch fuer die Queue genutzt wird.
|
||||||
// Wenn eine query() laeuft, puffert die Bridge die Nachricht intern.
|
|
||||||
async function dispatchMessage(text: string) {
|
async function dispatchMessage(text: string) {
|
||||||
// Auto-Session erstellen falls keine aktiv
|
// Auto-Session erstellen falls keine aktiv
|
||||||
let sessionId = get(currentSessionId);
|
let sessionId = get(currentSessionId);
|
||||||
|
|
@ -817,27 +794,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @-Mentions aufloesen: @pfad/datei.ts durch Dateiinhalt ersetzen
|
// Pruefen ob die Nachricht schon als queued im Chat steht
|
||||||
let resolvedText = text;
|
const existingQueued = get(messages).find((m) => m.queued && m.content === text);
|
||||||
const mentionPattern = /@([\w.\-/]+(?:#\d+(?:-\d+)?)?)/g;
|
if (existingQueued) {
|
||||||
const mentions = [...text.matchAll(mentionPattern)];
|
// Queued-Markierung entfernen — Nachricht ist jetzt aktiv
|
||||||
for (const match of mentions) {
|
messages.update((msgs) =>
|
||||||
const ref = match[1];
|
msgs.map((m) => m.id === existingQueued.id ? { ...m, queued: false } : m)
|
||||||
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 {
|
// In DB speichern (wurde vorher nicht gespeichert da queued)
|
||||||
// Datei nicht gefunden — @-Mention unverändert lassen
|
await saveMessageToDb({ ...existingQueued, queued: false });
|
||||||
}
|
} else {
|
||||||
}
|
// Neue Nachricht hinzufuegen (normaler Send, nicht aus Queue)
|
||||||
|
|
||||||
// User-Nachricht sofort im Chat anzeigen + in DB speichern
|
|
||||||
const msgId = crypto.randomUUID();
|
const msgId = crypto.randomUUID();
|
||||||
const msg: Message = {
|
const msg: Message = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
|
|
@ -846,23 +813,18 @@
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
messages.update((msgs) => [...msgs, msg]);
|
messages.update((msgs) => [...msgs, msg]);
|
||||||
|
// Sofort speichern (nicht auf Subscriber warten)
|
||||||
await saveMessageToDb(msg);
|
await saveMessageToDb(msg);
|
||||||
|
|
||||||
$currentInput = '';
|
|
||||||
|
|
||||||
// isProcessing nur setzen wenn nicht schon aktiv
|
|
||||||
// (bei gepufferten Nachrichten laeuft Claude ja schon)
|
|
||||||
if (!$isProcessing) {
|
|
||||||
$isProcessing = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$currentInput = '';
|
||||||
|
$isProcessing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Aufgeloesten Text an Claude senden (mit eingebetteten Datei-Inhalten)
|
await invoke('send_message', { message: text });
|
||||||
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}`);
|
||||||
// Nur auf false setzen wenn keine weiteren Nachrichten pending
|
|
||||||
$isProcessing = false;
|
$isProcessing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -874,12 +836,6 @@
|
||||||
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();
|
||||||
|
|
@ -1245,32 +1201,6 @@
|
||||||
{/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}
|
||||||
|
|
@ -1278,12 +1208,6 @@
|
||||||
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">
|
||||||
|
|
@ -1315,11 +1239,18 @@
|
||||||
<span class="transcript-text">{liveTranscript}</span>
|
<span class="transcript-text">{liveTranscript}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $messageQueue.length > 0}
|
||||||
|
<div class="queued-pill" title="Wird gesendet sobald Claude die aktuelle Antwort fertig hat">
|
||||||
|
<span class="queued-icon">📬</span>
|
||||||
|
<span class="queued-text">{$messageQueue.length} Nachricht{$messageQueue.length > 1 ? 'en' : ''} in der Queue</span>
|
||||||
|
<button class="queued-cancel" onclick={cancelQueued} aria-label="Wartende Nachrichten verwerfen">✕</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={inputTextarea}
|
bind:this={inputTextarea}
|
||||||
bind:value={$currentInput}
|
bind:value={$currentInput}
|
||||||
onkeydown={handleKeydown}
|
onkeydown={handleKeydown}
|
||||||
placeholder={$isProcessing ? 'Weiter tippen — wird nach aktuellem Turn verarbeitet...' : 'Nachricht eingeben... (Ctrl+K = Quick-Actions, Ctrl+Enter = Senden)'}
|
placeholder={$isProcessing ? 'Nachricht eingeben — wird nach Antwort automatisch gesendet' : 'Nachricht eingeben... (Ctrl+K = Quick-Actions, Ctrl+Enter = Senden)'}
|
||||||
disabled={isRecording}
|
disabled={isRecording}
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
@ -2036,41 +1967,6 @@
|
||||||
/* 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,28 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// DiffView — Zeigt Unterschiede zwischen altem und neuem Code
|
// DiffView — Zeigt Unterschiede zwischen altem und neuem Code
|
||||||
// Mit optionalen Accept/Reject-Buttons fuer interaktiven Modus
|
// Verwendet für Edit-Tool Ergebnisse
|
||||||
|
|
||||||
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 {
|
let { oldText, newText, filename = '', language = '' }: Props = $props();
|
||||||
oldText,
|
|
||||||
newText,
|
|
||||||
filename = '',
|
|
||||||
language = '',
|
|
||||||
interactive = false,
|
|
||||||
toolId = '',
|
|
||||||
onAccept,
|
|
||||||
onReject,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
// Einfache Diff-Berechnung (zeilenbasiert)
|
// Einfache Diff-Berechnung (zeilenbasiert)
|
||||||
interface DiffLine {
|
interface DiffLine {
|
||||||
|
|
@ -44,7 +31,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]) {
|
||||||
// Unveraenderte Zeile
|
// Unveränderte 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',
|
||||||
|
|
@ -55,7 +42,7 @@
|
||||||
newIdx++;
|
newIdx++;
|
||||||
lcsIdx++;
|
lcsIdx++;
|
||||||
} else {
|
} else {
|
||||||
// Neue Zeile hinzugefuegt
|
// Neue Zeile hinzugefügt
|
||||||
result.push({
|
result.push({
|
||||||
type: 'added',
|
type: 'added',
|
||||||
lineNo: { old: null, new: newIdx + 1 },
|
lineNo: { old: null, new: newIdx + 1 },
|
||||||
|
|
@ -72,7 +59,7 @@
|
||||||
});
|
});
|
||||||
oldIdx++;
|
oldIdx++;
|
||||||
} else if (newIdx < newLines.length) {
|
} else if (newIdx < newLines.length) {
|
||||||
// Zeile hinzugefuegt
|
// Zeile hinzugefügt
|
||||||
result.push({
|
result.push({
|
||||||
type: 'added',
|
type: 'added',
|
||||||
lineNo: { old: null, new: newIdx + 1 },
|
lineNo: { old: null, new: newIdx + 1 },
|
||||||
|
|
@ -118,7 +105,7 @@
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff berechnen wenn sich Inputs aendern
|
// Diff berechnen wenn sich Inputs ändern
|
||||||
let diffLines = $derived(computeDiff(oldText, newText));
|
let diffLines = $derived(computeDiff(oldText, newText));
|
||||||
|
|
||||||
// Statistiken
|
// Statistiken
|
||||||
|
|
@ -127,81 +114,24 @@
|
||||||
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" class:interactive>
|
<div class="diff-view">
|
||||||
<div class="diff-header">
|
|
||||||
<div class="diff-header-left">
|
|
||||||
{#if filename}
|
{#if filename}
|
||||||
<span class="filename" title={filename}>{shortenPath(filename)}</span>
|
<div class="diff-header">
|
||||||
{/if}
|
<span class="filename">{filename}</span>
|
||||||
{#if language}
|
{#if language}
|
||||||
<span class="language">{language}</span>
|
<span class="language">{language}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="stats">
|
<span class="stats">
|
||||||
<span class="stat-added">+{stats.added}</span>
|
<span class="added">+{stats.added}</span>
|
||||||
<span class="stat-removed">-{stats.removed}</span>
|
<span class="removed">-{stats.removed}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="diff-header-right">
|
|
||||||
{#if diffLines.length > 20}
|
|
||||||
<button class="btn-toggle" onclick={() => showFullDiff = !showFullDiff}>
|
|
||||||
{showFullDiff ? 'Kompakt' : 'Alles zeigen'}
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/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 visibleLines as { line, idx } (idx)}
|
{#each diffLines 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>
|
||||||
|
|
@ -223,42 +153,18 @@
|
||||||
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;
|
||||||
justify-content: space-between;
|
gap: var(--spacing-sm);
|
||||||
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 {
|
||||||
|
|
@ -270,66 +176,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-added { color: var(--success); }
|
.stats .added {
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
color: var(--success);
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-accept:hover {
|
.stats .removed {
|
||||||
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: 400px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -376,13 +239,4 @@
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
<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,18 +59,6 @@ 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,13 +25,11 @@ 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)
|
||||||
|
|
@ -461,38 +459,6 @@ 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