fix: Drawer bleibt offen + Auto-Scroll MutationObserver + KB-Hints Relevanz [appimage]
Some checks failed
Build AppImage / build (push) Has been cancelled

- Drawer: Backdrop entfernt, Panel bleibt fest offen bis X/Esc/Toggle
- Auto-Scroll: MutationObserver statt ResizeObserver (feuert bei jeder
  DOM-Aenderung), $effect scrollt nach tick() statt sofort
- KB-Hints: 60+ generische Stoppwoerter ergaenzt (bleibt, offen, rechts...),
  Relevanz-Schwelle 1.5 filtert zu generische Treffer raus

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-05-02 22:22:10 +02:00
parent c2f2cae2ef
commit f80f37884c
3 changed files with 50 additions and 50 deletions

View file

@ -193,6 +193,17 @@ const STOP_WORDS: &[&str] = &[
"schau", "guck", "check", "prüf", "teste", "versuch",
"können", "müssen", "sollen", "wollen", "dürfen",
"noch", "schon", "gerade", "gleich", "erstmal", "nochmal",
// Generische Verben/Adjektive die keine guten Suchbegriffe sind
"bleibt", "bleiben", "blieb", "kommt", "kommen", "kam", "macht", "machen",
"steht", "stehen", "stand", "liegt", "liegen", "lag", "nimmt", "nehmen",
"sieht", "sehen", "sah", "findet", "finden", "fand", "brauche", "braucht",
"offen", "öffnet", "öffnen", "geöffnet", "geschlossen", "schließen", "schließt",
"rechts", "links", "oben", "unten", "vorne", "hinten",
"neue", "neuer", "neues", "neuem", "neuen", "alte", "alter", "altes",
"ganze", "ganzen", "ganzer", "ganzes", "selbe", "selben", "selber", "selbes",
"gleiche", "gleichen", "gleicher", "gleiches", "andere", "anderen", "anderer",
"kaputt", "richtig", "fertig", "bereit", "leer",
"wenn", "weil", "damit", "dass", "sobald",
];
// ============ Konzept-Erkennung (wie Google/Facebook Textanalyse) ============
@ -585,10 +596,23 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O
return Ok(String::new());
}
// Relevanz-Schwelle: Ergebnisse mit zu niedrigem Score rausfiltern.
// MySQL FULLTEXT NATURAL LANGUAGE MODE gibt Scores > 0 zurück,
// aber generische Einzel-Wort-Treffer haben oft Score < 2.
let min_relevance = 1.5;
let relevant: Vec<_> = results.into_iter()
.filter(|(_, _, _, _, _, _, relevance)| *relevance >= min_relevance)
.collect();
if relevant.is_empty() {
println!("🔍 Keine Hints über Relevanz-Schwelle ({:.1})", min_relevance);
return Ok(String::new());
}
// Bereits gezeigte IDs filtern
let filtered: Vec<_> = {
let topic = SESSION_TOPIC.lock().unwrap();
results.into_iter()
relevant.into_iter()
.filter(|(id, _, _, _, _, _, _)| !topic.shown_ids.contains(id))
.take(limit)
.collect()

View file

@ -4,14 +4,14 @@
// Auto-Scroll nicht mehr zum Ende. Stattdessen erscheint ein
// Back-to-Bottom-Button.
//
// Phase 9.1: ResizeObserver am Container — feuert auch bei
// Tool-Card-Slide-In, Diff-Aufklappen, Markdown-Code-Blocks. Tracker
// liest jetzt zusaetzlich die Anzahl Tool-Calls.
// Phase 9.2: MutationObserver statt ResizeObserver — feuert bei jeder
// DOM-Aenderung (neue Messages, WorkingIndicator, Tool-Cards, Markdown).
// $effect-Tracker scrollt nach tick() damit DOM schon gerendert ist.
import { messages, isProcessing, type Message as ChatMessage } from '$lib/stores';
import MessageItem from './Message.svelte';
import WorkingIndicator from './WorkingIndicator.svelte';
import { onMount, onDestroy } from 'svelte';
import { onMount, onDestroy, tick } from 'svelte';
interface Props {
sessionId?: string | null;
@ -34,7 +34,7 @@
let userScrolledUp = $state(false);
let autoScrolling = false; // Guard: ignoriert onscroll waehrend wir selbst scrollen
let autoScrollTimer: ReturnType<typeof setTimeout> | null = null;
let resizeObs: ResizeObserver | null = null;
let mutationObs: MutationObserver | null = null;
// Working-Indicator: zeigen wenn Claude verarbeitet, aber noch keine
// Assistant-Tokens da sind (vor erstem Stream + waehrend Tool-Calls).
@ -96,9 +96,9 @@
scrollToBottom(true);
}
// Reactive-Tracker: deckt jetzt auch Tool-Calls ab. Sobald sich die
// Anzahl Tool-Calls in der letzten Message aendert (Slide-In, Status
// running→done), wird Auto-Scroll getriggert.
// Reactive-Tracker: deckt Messages, Tool-Calls, Parts und Processing ab.
// Nach tick() scrollen, damit DOM-Aenderungen (WorkingIndicator, neue
// Messages) schon gerendert sind bevor scrollHeight gelesen wird.
$effect(() => {
const last = $messages[$messages.length - 1];
const _trackers = [
@ -106,7 +106,6 @@
$isProcessing,
last?.content?.length ?? 0,
last?.parts?.length ?? 0,
// Letzten Part tracken — bei Streaming waechst dessen content
(last?.parts && last.parts.length > 0)
? last.parts[last.parts.length - 1]?.content?.length ?? 0
: 0,
@ -114,7 +113,7 @@
last?.toolCalls?.map((t) => t.status).join(',') ?? '',
];
void _trackers;
scrollToBottom();
tick().then(() => scrollToBottom());
});
// Wenn der User eine neue Message schreibt und die Antwort ankommt, soll
@ -146,35 +145,27 @@
});
onMount(() => {
// ResizeObserver fuer den Container: feuert wenn sich die Hoehe
// aendert (Tool-Card aufklappen, Diff-Erweitern, Code-Block rendern).
// Ohne das wuerde der Stream "abgehaengt" weil $effect nur bei
// Content-Length-Aenderung greift.
if (container && typeof ResizeObserver !== 'undefined') {
resizeObs = new ResizeObserver(() => {
// MutationObserver: feuert bei JEDER DOM-Aenderung im Container —
// neue Messages, WorkingIndicator ein/aus, Tool-Card-Expansion,
// Markdown-Rendering, Diff-Aufklappen. Robuster als ResizeObserver
// (Container-Groesse ist bei overflow:auto konstant, nur scrollHeight
// waechst — ResizeObserver feuert da nicht).
if (container) {
mutationObs = new MutationObserver(() => {
if (!userScrolledUp) scrollToBottom();
});
// Den letzten Child beobachten, nicht den Container selbst —
// Container-Groesse ist konstant, sein Inhalt waechst.
const inner = container.firstElementChild;
if (inner) {
// Alle direkten Kinder beobachten ist zu teuer. Ein Wrapper
// reicht — der Container hat als Direct-Child die Message-
// Liste, und sein scrollHeight aendert sich passend.
// Wir beobachten den Container selbst — ResizeObserver
// feuert auch wenn der Inhalt waechst (clientHeight stays,
// scrollHeight grows → checkScroll triggert).
}
// Robusteste Variante: Container observed, plus Mutation-Observer
// fuer Content-Aenderungen.
resizeObs.observe(container);
mutationObs.observe(container, {
childList: true,
subtree: true,
characterData: true,
});
}
});
onDestroy(() => {
if (resizeObs) {
resizeObs.disconnect();
resizeObs = null;
if (mutationObs) {
mutationObs.disconnect();
mutationObs = null;
}
releaseAutoScroll();
});

View file

@ -1,6 +1,6 @@
<script lang="ts">
// Rechts-eingeschobenes Panel fuer Werkzeug-Tabs.
// Esc schliesst, Klick auf Backdrop schliesst.
// Bleibt fest offen bis explizit geschlossen (X-Button, Esc, erneuter Sidebar-Klick).
//
// Nutzung:
// <Drawer open={openDrawer === 'memory'} onClose={() => openDrawer = null}>
@ -33,13 +33,6 @@
</script>
{#if open}
<div
class="backdrop"
role="presentation"
onclick={() => onClose?.()}
onkeydown={() => {}}
aria-hidden="true"
></div>
<aside class="drawer" style="width: {width}px" role="dialog" aria-label={title ?? 'Drawer'}>
{#if title}
<header class="head">
@ -54,14 +47,6 @@
{/if}
<style>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 80;
animation: fade-in var(--dur-fast) var(--ease);
}
.drawer {
position: fixed;
top: 0;