Compare commits
No commits in common. "09a9513983fce4e619b3d4b599373f9c5b0348fe" and "fc71e6c63467d01c3266cf06199c34556d243a85" have entirely different histories.
09a9513983
...
fc71e6c634
5 changed files with 45 additions and 163 deletions
107
ROADMAP.md
107
ROADMAP.md
|
|
@ -1,6 +1,6 @@
|
|||
# Claude Desktop — Roadmap
|
||||
|
||||
Stand: 28.04.2026
|
||||
Stand: 27.04.2026
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -155,111 +155,6 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
|
|||
|
||||
---
|
||||
|
||||
## Phase 16: Schulungsmodus-Ausbau — Session-Replay
|
||||
|
||||
**Ziel:** Das Praesentations-Fenster wird vom Slide-Stream zum vollwertigen Session-Replay. Drei Spalten: Datei-Baum links (waechst mit den beruehrten Pfaden), Workspace in der Mitte (animierter Edit oder Diff-Viewer), Timeline rechts (klickbare Liste aller Tool-Aktionen). Live waehrend Claude arbeitet, retroaktiv fuer gespeicherte Sessions.
|
||||
|
||||
**Architektur-Entscheidung:** Bridge-passiv vor MCP-Tools. Die Bridge sieht ohnehin jeden Tool-Call — sie schreibt einfach mit. Claude muss nichts wissen oder steuern. MCP-Tools fuer gezielte Erklaerung sind optional und kommen erst wenn die passive Erfassung sauber laeuft.
|
||||
|
||||
**Tradeoff:** Reine Bridge-Loesung ist robust und funktioniert auch retroaktiv, aber „stumm" — sie zeigt was passiert, nicht warum. Reine MCP-Steuerung waere erklaerend, aber fragil (Claude muss konsistent zugreifen). Hybrid in dieser Reihenfolge gibt sofort 90 % des Werts und laesst sich spaeter erweitern.
|
||||
|
||||
### Phase 16.1: Bridge-Telemetrie (Datenerfassung)
|
||||
|
||||
**Aufwand:** ~3-4 h. Ohne UI-Aenderungen — am Ende der Phase liegen Daten in SQLite.
|
||||
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| ⏳ SQLite-Schema `session_timeline` | `db.rs`, `migrations` | id, session_id, tool_id, tool, file_path, timestamp, action (read/edit/write/glob/grep/bash), before_hash, after_hash, line_start, line_end, payload_json |
|
||||
| ⏳ Telemetry-Modul | `telemetry.rs` (NEU) | `record_tool_event(session_id, ev)`, dedupliziert nach tool_id, schreibt in `session_timeline` |
|
||||
| ⏳ Hook in claude.rs | `claude.rs` | In `handle_bridge_message` bei `tool-start`/`tool-end`/`checkpoint-after` jeweils `telemetry::record_*` aufrufen |
|
||||
| ⏳ Before/After-Snapshots | `db.rs` | Zusaetzlich zu Checkpoint-Tabelle: dauerhafter Diff pro Edit gespeichert (komprimiert), nicht nur fuer Rewind |
|
||||
| ⏳ Tauri-Command `presentation_get_timeline` | `teaching.rs` | Liefert Timeline einer Session (live oder historisch) |
|
||||
| ⏳ Tauri-Event `timeline-append` | `claude.rs` | Pro neuem Eintrag — Frontend kann live mitschreiben |
|
||||
|
||||
**Risiko:** Datenmenge bei langen Sessions. Mitigation: Before/After als komprimiertes Diff (zstd), nicht als Volltext doppelt.
|
||||
|
||||
### Phase 16.2: 3-Spalten-Layout im Praesentations-Fenster
|
||||
|
||||
**Aufwand:** ~2-3 h. Reines Layout — die Spalten zeigen erstmal Platzhalter, Logik kommt in 16.3-16.5.
|
||||
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| ⏳ PaneForge-Layout | `presentation/+page.svelte` | 3 PaneGroups: 22 % / 56 % / 22 %, resizable, persistente Breiten |
|
||||
| ⏳ Mode-Toggle | `presentation/+page.svelte` | Knopf in Footer: „Slides" (alter Modus) ↔ „Replay" (neues Layout). Slide-Modus bleibt fuer Mermaid/Code/Text erhalten |
|
||||
| ⏳ Container-Komponenten | `FileTree.svelte`, `Workspace.svelte`, `Timeline.svelte` (NEU) | Skelette, in den naechsten Phasen befuellt |
|
||||
| ⏳ Default-View | `presentation/+page.svelte` | Bei offenem Fenster und neuer Tool-Aktion automatisch in Replay-Modus wechseln |
|
||||
|
||||
### Phase 16.3: Datei-Baum mit Live-Markierung
|
||||
|
||||
**Aufwand:** ~3 h.
|
||||
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| ⏳ Tree-Aufbau aus Timeline | `FileTree.svelte` | Pfade aus Timeline-Eintraegen → hierarchischer Baum, Ordner-Knoten implizit |
|
||||
| ⏳ Status-Farbcodierung | `FileTree.svelte` | grau=unbekannt, blau=gelesen, gruen=geaendert, gelb=erstellt, rot=geloescht, pulsierend=aktuell aktiv |
|
||||
| ⏳ Auto-Expand bei Aktivitaet | `FileTree.svelte` | Wenn Datei in der Timeline auftaucht, Pfad bis zur Wurzel auffalten |
|
||||
| ⏳ Klick auf Datei | `FileTree.svelte` → `Workspace.svelte` | Mitte zeigt letzten bekannten Stand + alle Edits dieser Datei untereinander |
|
||||
| ⏳ Mini-Counter pro Knoten | `FileTree.svelte` | „edited.ts (3 ✏️)" — Anzahl Aenderungen direkt am Knoten |
|
||||
|
||||
**Risiko:** Bei sehr breiten Repos wird der Tree unuebersichtlich. Mitigation: Nur Pfade die in der Timeline auftauchen — kein vollstaendiger Filesystem-Scan. Maximal 200 Knoten sichtbar, Rest collapsed.
|
||||
|
||||
### Phase 16.4: Workspace (Mitte) — Edit-Replay
|
||||
|
||||
**Aufwand:** ~2 h. Bauteile (`AnimatedFileEdit`) sind da, fehlen nur Verkettung und Auswahl-Logik.
|
||||
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| ⏳ Aktive Datei-Anzeige | `Workspace.svelte` | Eine Datei zur Zeit, Header mit Pfad + „aktiv"/„abgeschlossen" Status |
|
||||
| ⏳ Edit-Liste pro Datei | `Workspace.svelte` | Wenn mehrere Edits an gleicher Datei: chronologische Liste, jeder Edit als Karte mit Before/After-Diff |
|
||||
| ⏳ Replay-Knopf pro Edit | `Workspace.svelte`, `AnimatedFileEdit.svelte` | Klick spielt die Animation erneut ab — `{#key}` fuer Reset |
|
||||
| ⏳ Lese-Operationen anzeigen | `Workspace.svelte` | Read/Glob/Grep nicht animiert sondern als kompakte Eintraege („Gelesen: Zeilen 5-50") |
|
||||
| ⏳ Bash-Snippets | `Workspace.svelte` | `Bash`-Calls als Terminal-Card mit Befehl + Output-Snippet |
|
||||
|
||||
### Phase 16.5: Timeline (Rechts) — Chronologische Navigation
|
||||
|
||||
**Aufwand:** ~2 h.
|
||||
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| ⏳ Vertikale Timeline | `Timeline.svelte` | Chronologisch, neueste oben, Zeitstempel + Tool-Icon + Pfad-Snippet |
|
||||
| ⏳ Filter-Chips | `Timeline.svelte` | Nur Edits / Nur Reads / Nur Bash / Alle — schmale Toggle-Reihe |
|
||||
| ⏳ Klick → Workspace + Tree | `Timeline.svelte` | Klick aktiviert Datei in Tree und zeigt diesen Edit in Mitte |
|
||||
| ⏳ Live-Indikator | `Timeline.svelte` | Pulsierender Punkt am neuesten Eintrag waehrend Session laeuft |
|
||||
| ⏳ Session-Wechsler | `Timeline.svelte` | Dropdown oben: aktuelle Session / historische Sessions, laedt jeweilige Timeline |
|
||||
|
||||
### Phase 16.6: Optional — MCP-Tools fuer gezielte Erklaerung
|
||||
|
||||
**Aufwand:** ~3 h, optional. Erst nach 16.1-16.5 sinnvoll, vorher kein Mehrwert.
|
||||
|
||||
**Konzept:** Wenn das Praesentations-Fenster offen ist, bekommt Claude per System-Prompt-Hint einen MCP-Server `presentation` mit drei Tools. Sie kann sie nutzen — muss aber nicht. Bridge-Telemetrie laeuft unabhaengig weiter.
|
||||
|
||||
| Tool | Wirkung |
|
||||
|------|---------|
|
||||
| `present_focus(file, line_start?, line_end?)` | Markiert Datei + Zeilenbereich im Workspace, scrollt dorthin |
|
||||
| `present_explain(text, anchor?)` | Sprechblase rechts neben Workspace, optional an Datei/Zeile gepinnt |
|
||||
| `present_diagram(mermaid)` | Modal mit Mermaid-Diagramm — fuer Architektur/Flow-Erklaerungen |
|
||||
|
||||
**Tradeoff:** Claude muss daran denken, das ist anfangs flackernd. System-Prompt sollte „nutze diese Tools sparsam, nur fuer Schluesselstellen" sagen, sonst wird's Spam.
|
||||
|
||||
### Phase 16.7: Polish + Export
|
||||
|
||||
**Aufwand:** ~2 h.
|
||||
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| ⏳ Geschwindigkeitssteuerung global | `presentation/+page.svelte` | Speed-Steps wirken jetzt fuer alle Edits — auch beim Replay alter Eintraege |
|
||||
| ⏳ Pause/Resume bei Live | `presentation/+page.svelte` | „Pause" friert Tree+Timeline ein, neue Eintraege werden gepuffert, „Resume" arbeitet sie ab |
|
||||
| ⏳ Session-Export | `teaching.rs` | „Schulungsvideo" als JSON-Bundle exportieren — laed't in einer anderen Instanz wieder |
|
||||
| ⏳ Hotkeys | `presentation/+page.svelte` | j/k = Timeline rauf/runter, Enter = Replay, t = Tree-Fokus |
|
||||
|
||||
---
|
||||
|
||||
**Gesamt-Aufwand Phase 16 (ohne 16.6):** ~14-16 h, in 6 zusammenhaengenden Sub-Phasen. Jede Sub-Phase ist fuer sich nuetzlich — auch wenn nur 16.1-16.3 fertig werden, ist das schon ein lebender Datei-Baum mit Telemetrie-Backend.
|
||||
|
||||
**Reihenfolge-Begruendung:** Telemetrie zuerst (16.1) — ohne Daten kein UI. Layout-Skelett (16.2) gibt einen Rahmen zum Befuellen. Dann von links nach rechts (16.3 Tree → 16.4 Workspace → 16.5 Timeline) weil das die natuerliche Lesereihenfolge des Users ist. Polish (16.7) am Ende. MCP (16.6) nur wenn Phase 16.1-16.5 stabil sind.
|
||||
|
||||
---
|
||||
|
||||
## Technische Schulden
|
||||
|
||||
| Was | Prioritaet |
|
||||
|
|
|
|||
|
|
@ -68,9 +68,9 @@ impl KbCache {
|
|||
}
|
||||
}
|
||||
|
||||
// Globaler Cache — 15 Sekunden TTL (kurz genug damit thematische Wechsel ankommen)
|
||||
// Globaler Cache — 60 Sekunden TTL
|
||||
static KB_CACHE: std::sync::LazyLock<Mutex<KbCache>> =
|
||||
std::sync::LazyLock::new(|| Mutex::new(KbCache::new(15)));
|
||||
std::sync::LazyLock::new(|| Mutex::new(KbCache::new(60)));
|
||||
|
||||
// ============ Smart Hints v2: Session Topic Tracking ============
|
||||
// Akkumuliert Keywords über die Session hinweg, verhindert Wiederholungen,
|
||||
|
|
@ -314,19 +314,29 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
|
|||
}
|
||||
}
|
||||
|
||||
// Keywords der aktuellen Nachricht merken (fuer shown_ids Tracking)
|
||||
// NICHT akkumulieren — sonst wird die Query generisch und liefert immer
|
||||
// die gleichen "Favoriten"-Eintraege statt thematisch passende Hints.
|
||||
topic.keywords = new_keywords.clone();
|
||||
// Keywords zur Session hinzufügen (dedupliziert)
|
||||
for kw in &new_keywords {
|
||||
if !topic.keywords.contains(kw) {
|
||||
topic.keywords.push(kw.clone());
|
||||
}
|
||||
}
|
||||
// Max 20 Keywords behalten (neueste bevorzugt)
|
||||
if topic.keywords.len() > 20 {
|
||||
let start = topic.keywords.len() - 20;
|
||||
topic.keywords = topic.keywords[start..].to_vec();
|
||||
}
|
||||
|
||||
topic.message_count += 1;
|
||||
|
||||
// Alle 2 Nachrichten frische Hints liefern (statt alle 3)
|
||||
// Nur alle 3 Nachrichten frische Hints liefern (nicht jedes Mal!)
|
||||
// Erste Nachricht (count=1) bekommt immer Hints
|
||||
let skip = topic.message_count > 1 && topic.message_count % 2 != 0;
|
||||
let skip = topic.message_count > 1 && topic.message_count % 3 != 1;
|
||||
|
||||
// Query aus aktuellen Keywords — direkt und thematisch
|
||||
let query = if new_keywords.is_empty() {
|
||||
// Session-Query bauen: letzte 6 Keywords für breiteren Kontext
|
||||
let query = if topic.keywords.len() > 3 {
|
||||
let start = topic.keywords.len().saturating_sub(6);
|
||||
topic.keywords[start..].join(" ")
|
||||
} else if new_keywords.is_empty() {
|
||||
safe_truncate(query, 100).to_string()
|
||||
} else {
|
||||
new_keywords.join(" ")
|
||||
|
|
@ -389,13 +399,12 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O
|
|||
WHERE status = 'active'
|
||||
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||
ORDER BY
|
||||
CASE WHEN tags LIKE ? THEN 10 ELSE 0 END +
|
||||
(MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) * 100) +
|
||||
(priority * 5) +
|
||||
LEAST(usage_count, 50)
|
||||
DESC
|
||||
CASE WHEN tags LIKE ? THEN 1 ELSE 0 END DESC,
|
||||
priority DESC,
|
||||
usage_count DESC,
|
||||
relevance DESC
|
||||
LIMIT ?"#,
|
||||
(search_query, search_query, &proj_pattern, search_query, fetch_limit),
|
||||
(search_query, search_query, &proj_pattern, fetch_limit),
|
||||
).await.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
conn.exec(
|
||||
|
|
@ -407,13 +416,9 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O
|
|||
FROM knowledge
|
||||
WHERE status = 'active'
|
||||
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||
ORDER BY
|
||||
(MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) * 100) +
|
||||
(priority * 5) +
|
||||
LEAST(usage_count, 50)
|
||||
DESC
|
||||
ORDER BY priority DESC, usage_count DESC, relevance DESC
|
||||
LIMIT ?"#,
|
||||
(search_query, search_query, search_query, fetch_limit),
|
||||
(search_query, search_query, fetch_limit),
|
||||
).await.map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -388,17 +388,22 @@
|
|||
let silenceStartTime: number | null = null;
|
||||
let vadEnabled = $state(true); // VAD ein/aus
|
||||
|
||||
// Scroll wird komplett von MessageList verwaltet (hat den echten scroll-Container).
|
||||
// ChatPanel.scrollToBottom() war auf messagesContainer (Wrapper ohne overflow) und
|
||||
// tat nichts — entfernt um Konflikte mit MessageList zu vermeiden.
|
||||
async function scrollToBottom() {
|
||||
await tick();
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Bei Session-Wechsel: Compacting-Flag + KB-Hints zurücksetzen
|
||||
$effect(() => {
|
||||
if ($messages.length) scrollToBottom();
|
||||
});
|
||||
|
||||
// Bei Session-Wechsel: Compacting-Flag zurücksetzen
|
||||
$effect(() => {
|
||||
if ($currentSessionId) {
|
||||
compactingWarningShown = false;
|
||||
showCompactingDialog = false;
|
||||
// KB-Hints Session-Topic zurücksetzen damit neue Hints kommen
|
||||
invoke('reset_kb_session').catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1194,7 +1199,6 @@
|
|||
|
||||
<div class="chat-messages-wrap" bind:this={messagesContainer}>
|
||||
<MessageList
|
||||
sessionId={$currentSessionId}
|
||||
{streamingMessageId}
|
||||
onEdit={handleEditById}
|
||||
onRegenerate={handleRegenerateById}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
sessionId?: string | null;
|
||||
streamingMessageId?: string | null;
|
||||
onEdit?: (id: string) => void;
|
||||
onRegenerate?: (id: string) => void;
|
||||
|
|
@ -22,7 +21,6 @@
|
|||
onRewind?: (id: string) => void;
|
||||
}
|
||||
let {
|
||||
sessionId = null,
|
||||
streamingMessageId = null,
|
||||
onEdit,
|
||||
onRegenerate,
|
||||
|
|
@ -84,10 +82,10 @@
|
|||
autoScrollTimer = setTimeout(releaseAutoScroll, 700);
|
||||
} else {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
// Instant-Scroll: Guard sofort nach einem rAF loesen.
|
||||
// Zwei rAFs waren zu langsam bei schnellem Streaming — der naechste
|
||||
// Token kam bevor der Guard aufgehoben war.
|
||||
requestAnimationFrame(releaseAutoScroll);
|
||||
// Instant-Scroll: ein rAF reicht damit das onscroll-Event durch ist
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(releaseAutoScroll);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,16 +130,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Session-Wechsel: Scroll-State zuruecksetzen damit neue Session
|
||||
// immer am Ende angezeigt wird (nicht vom alten userScrolledUp blockiert)
|
||||
$effect(() => {
|
||||
if (sessionId) {
|
||||
userScrolledUp = false;
|
||||
lastRole = null;
|
||||
requestAnimationFrame(() => scrollToBottom(true));
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
// ResizeObserver fuer den Container: feuert wenn sich die Hoehe
|
||||
// aendert (Tool-Card aufklappen, Diff-Erweitern, Code-Block rendern).
|
||||
|
|
|
|||
|
|
@ -435,19 +435,9 @@ export function dbToMessage(db: DbMessage): Message {
|
|||
};
|
||||
}
|
||||
|
||||
// Nachrichten aus DB in Store laden — mit Sortierung und Merge-Schutz
|
||||
// Nachrichten aus DB in Store laden
|
||||
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
||||
const mapped = dbMessages.map(dbToMessage);
|
||||
mapped.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||
messages.update(existing => {
|
||||
// Wenn waehrend des Ladens schon neue Messages reinkamen (Streaming): mergen
|
||||
const dbIds = new Set(mapped.map(m => m.id));
|
||||
const newOnly = existing.filter(m => !dbIds.has(m.id));
|
||||
if (newOnly.length === 0) return mapped;
|
||||
return [...mapped, ...newOnly].sort((a, b) =>
|
||||
a.timestamp.getTime() - b.timestamp.getTime()
|
||||
);
|
||||
});
|
||||
messages.set(dbMessages.map(dbToMessage));
|
||||
}
|
||||
|
||||
// ============ System-Monitor ============
|
||||
|
|
|
|||
Loading…
Reference in a new issue