All checks were successful
Build AppImage / build (push) Successful in 8m20s
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.
71 lines
2 KiB
Rust
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ä");
|
|
}
|
|
}
|