claude-desktop/src-tauri/src/strutil.rs
Eddy 79f4f9fb21
All checks were successful
Build AppImage / build (push) Successful in 8m20s
fix: UTF-8-Crash + Input-Reset + ApprovalBar + Scroll/Streaming-Polish [appimage]
Crash-Fix:
- src/db.rs:801 panickte mit "byte index 240 is not a char boundary"
  mitten in einem -Emoji → SIGABRT. Neues strutil-Modul mit
  safe_truncate()/safe_truncate_ellipsis() (5 Tests grün), an allen
  &s[..N]-Stellen in db/claude/knowledge/session/memory.rs eingebaut.
- update.rs: Stale Lock-Files vom letzten Crash werden jetzt
  protokolliert ("🧹 Stale Lock-Datei aus vorherigem Crash gefunden").

Chat-Polish:
- Input-Textfeld wird nach Senden zuverlässig geleert (Store-Reset +
  DOM-Reset + tick — Svelte 5 bind:value mit Auto-Subscription
  aktualisiert sonst nicht synchron).
- ApprovalBar.svelte (NEU): Sticky-Bar überm Input mit klar
  beschrifteten Buttons "Übernehmen"/"Verwerfen" statt mehrdeutigem
  "Behalten/Zurueck". Bleibt sichtbar wenn der Chat scrollt. Klick
  auf Datei-Name scrollt zur Inline-Karte und blinkt sie. Shortcuts
  Ctrl+Enter/Ctrl+Backspace.
- MessageList: Auto-Scroll trackt jetzt auch toolCalls.length und
  Status-Änderungen, plus ResizeObserver am Container. Smooth bei
  kleinen Distanzen, instant bei großen.
- Streaming-Caret: pulsierender Block-Cursor mit Glow-Shadow.
- Tool-Cards: Slide-In-Transition + Shimmer-Animation auf running.
- WorkingIndicator: Verb passt sich an processingPhase an.
2026-04-27 20:55:08 +02:00

71 lines
2 KiB
Rust

// UTF-8-sichere String-Truncation
//
// `&s[..n]` panickt wenn n mitten in einem Multi-Byte-Zeichen liegt.
// Konkret: ein '✅' belegt 3 Bytes; bei `&s[..240]` wenn ✅ auf
// Byte-Position 239..241 sitzt → Panic "is not a char boundary".
//
// Diese Funktion findet die naechste Char-Boundary <= max_bytes
// und schneidet dort sauber ab.
/// Schneidet `s` an einer Char-Boundary <= `max_bytes` ab.
/// Wenn `s` schon kuerzer ist, wird `s` unveraendert zurueckgegeben.
pub fn safe_truncate(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
// floor_char_boundary ist nightly; eigene Implementierung:
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
/// Wie `safe_truncate`, haengt aber `…` an wenn gekuerzt wurde.
pub fn safe_truncate_ellipsis(s: &str, max_bytes: usize) -> String {
if s.len() <= max_bytes {
s.to_string()
} else {
format!("{}", safe_truncate(s, max_bytes))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_works() {
assert_eq!(safe_truncate("hello world", 5), "hello");
}
#[test]
fn does_not_panic_on_emoji_boundary() {
// ✅ ist 3 Bytes (E2 9C 85). Wenn wir bei Byte 1 abschneiden
// wuerde &s[..1] panicken.
let s = "abc✅def";
// ✅ liegt auf Byte 3..6, abschneiden bei 4 → muss auf 3 zurueck
let out = safe_truncate(s, 4);
assert_eq!(out, "abc");
}
#[test]
fn ellipsis_appends() {
let s = "abcdefghij";
assert_eq!(safe_truncate_ellipsis(s, 5), "abcde…");
}
#[test]
fn ellipsis_no_truncate_when_short() {
assert_eq!(safe_truncate_ellipsis("hi", 10), "hi");
}
#[test]
fn handles_german_umlaut() {
// ä ist 2 Bytes. "abäc" → ab=2, ä=2 (4), c=1 (5)
let s = "abäc";
// Schnitt bei 3 → ä halbiert → muss auf 2 zurueck
assert_eq!(safe_truncate(s, 3), "ab");
assert_eq!(safe_truncate(s, 4), "abä");
}
}