From 68d2500037a9bd4dc164466d61155d3617e2f4d0 Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 21 Apr 2026 10:55:35 +0200 Subject: [PATCH] feat: Projekt-Wechsel, File-Drop, Persistent Memory [appimage] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Projekt-Wechsel UI in SessionList: Dropdown, Hinzufügen/Entfernen, auto Sticky-Context - File-Drop auf Chat: Text als Code-Block, Bilder als Base64, 500KB Limit - Persistent Memory: auto_load Einträge in Claude-Context injiziert (Cross-Session) - Memory CRUD Tauri-Commands: save/delete/list/autoload - Svelte 5 Syntax-Fix: on:click → onclick in SessionList.svelte Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 + ROADMAP.md | 6 +- src-tauri/src/claude.rs | 21 +- src-tauri/src/db.rs | 134 +++++++++- src-tauri/src/lib.rs | 9 + src-tauri/src/memory.rs | 37 +++ src/lib/components/ChatPanel.svelte | 157 +++++++++++- src/lib/components/SessionList.svelte | 353 +++++++++++++++++++++++++- 8 files changed, 709 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4ec57..c638cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). ## [Unreleased] - 2026-04-21 ### Hinzugefügt +- **Projekt-Wechsel**: Ein-Klick-Projektwechsel in der Sidebar — Dropdown mit Projektliste, Hinzufügen/Entfernen, Working-Dir + Sticky-Context wird automatisch umgeschaltet (`SessionList.svelte`, `db.rs`) +- **File-Drop auf Chat**: Dateien per Drag & Drop auf den Chat ziehen — Text-Dateien als Code-Block, Bilder als Base64, Spracherkennung, 500KB-Limit (`ChatPanel.svelte`) +- **Persistent Memory**: Auto-Load Memory-Einträge werden bei jeder Nachricht in den Claude-Context injiziert — Cross-Session Gedächtnis für Patterns, Zugänge, Präferenzen (`memory.rs`, `claude.rs`) +- **Memory CRUD-Commands**: Speichern, Löschen, Auflisten, Auto-Load-Filter für Memory-Einträge als Tauri-Commands (`memory.rs`, `lib.rs`) - **Quick-Actions Palette (Ctrl+K)**: VS-Code-artige Kommandopalette mit Suche, Kategorien (Build, Git, Session, Navigation, Voice, Tools), Keyboard-Navigation (`QuickActions.svelte`) - **Lokales Voice (Phase 2.2)**: whisper-cli STT + piper-tts TTS, komplett lokal ohne OpenAI-API (`voice.rs`, `VoicePanel.svelte`) - **Chat-Detach**: Chat in separates Fenster herauslösen, Platz für andere Panels, Zurückholen per Button (`chat_window.rs`, `+page.svelte`) @@ -33,6 +37,8 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). - `lib.rs`: App-Lifecycle erweitert um Lock-Datei create/remove bei Start/Exit ### Behoben +- **Svelte 5 Event-Syntax**: Alle `on:click` → `onclick` in SessionList.svelte (keine Mixed-Syntax mehr) +- **Mikrofon hängt**: PipeWire-Fallback + 5s getUserMedia-Timeout wenn PipeWire nicht läuft (`nix/default.nix`, `ChatPanel.svelte`) - **Update-Fortschrittsbalken**: Erreicht jetzt visuell 100% vor der Bestätigungsmeldung (`update.rs`, `UpdateDialog.svelte`) - **Mikrofon in Produktion**: GStreamer + PipeWire-Plugins fehlten im Nix-Wrapper, WebKitGTK konnte getUserMedia nicht nutzen (`nix/default.nix`) - Updater konnte Binary ersetzen während App noch lief (kein Lock, kein Prozess-Check) diff --git a/ROADMAP.md b/ROADMAP.md index 115efa2..c50ee7b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -60,10 +60,10 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig: | Feature | Datei(en) | Status | |---------|-----------|--------| -| Projekt-Wechsel | `context.rs`, UI | ⬜ Ein Klick wechselt Projekt (CWD, CLAUDE.md, Context, KB-Filter) | +| ✅ Projekt-Wechsel | `db.rs`, `SessionList.svelte` | Ein Klick wechselt Projekt (CWD, Context, KB-Filter) | | MCP-Hub nativ | `claude-bridge.js` | ⬜ Alle MCP-Server direkt nutzbar (Docker, Forgejo, DB) ohne CLI-Umweg | | Guard-Rails UI | `guard.rs`, `GuardPanel.svelte` | ⬜ Live-Anzeige was Claude darf/nicht darf, Ein-Klick-Freigabe | -| Persistent Memory | `memory.rs`, `db.rs` | ⬜ Cross-Session Gedaechtnis — Claude erinnert sich an ALLES | +| ✅ Persistent Memory | `memory.rs`, `claude.rs` | Auto-Load Eintraege in Context, Cross-Session Gedaechtnis | | ✅ Quick-Actions | `QuickActions.svelte`, `ChatPanel.svelte` | Ctrl+K Palette: Deploy, Build, Test, Commit, Git, Navigation | | ✅ Voice-Conversation | `voice.rs`, `VoicePanel.svelte` | Lokales Whisper STT + Piper TTS, VAD, Gespraechsmodus | | ✅ Settings-Panel | `SettingsPanel.svelte` | VS-Code-artiges Layout mit Suche, Kategorien, Commands, Hooks | @@ -93,7 +93,7 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig: |---------|-----------|--------------| | D-Bus Actions | `programs.rs` | Vordefinierte Aktionen: Dolphin oeffnen, Kate starten, Notifications | | Clipboard-Watch | neu: `clipboard.rs` | Claude reagiert auf Clipboard-Inhalt (Code-Snippet → erklaeren, URL → zusammenfassen) | -| File-Drop | `ChatPanel.svelte` | Dateien auf Chat droppen → Claude analysiert/bearbeitet | +| ✅ File-Drop | `ChatPanel.svelte` | Dateien auf Chat droppen → Claude analysiert/bearbeitet | | Screenshot-Analyse | `programs.rs` | Bildschirmbereich markieren → Claude beschreibt/debuggt UI | | Global Hotkey | `lib.rs` | Super+C oeffnet Claude-Eingabe von ueberall | diff --git a/src-tauri/src/claude.rs b/src-tauri/src/claude.rs index 3928b99..d12a090 100644 --- a/src-tauri/src/claude.rs +++ b/src-tauri/src/claude.rs @@ -487,11 +487,28 @@ fn load_sticky_context_for_prompt(app: &AppHandle) -> Option { } } + // Auto-Load Memory-Einträge anhängen (Persistent Memory) + let memory_entries = db.load_memory_entries().unwrap_or_default(); + let autoload: Vec<_> = memory_entries.into_iter().filter(|e| e.auto_load).collect(); + let mut memory_section = String::new(); + if !autoload.is_empty() { + memory_section.push_str("\n\n## Persistentes Gedächtnis\n"); + for entry in &autoload { + memory_section.push_str(&format!("- **{}** ({}): {}\n", + entry.key, + format!("{:?}", entry.category), + entry.value + )); + } + println!("🧠 {} Auto-Load Memory-Einträge in Context injiziert", autoload.len()); + } + let rendered = sticky.render(); - if rendered.is_empty() { + let combined = format!("{}{}", rendered, memory_section); + if combined.trim().is_empty() { None } else { - Some(rendered) + Some(combined) } } else { None diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index e147484..1495989 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -205,6 +205,19 @@ impl Database { WHERE timestamp < datetime('now', '-7 days'); END; + -- Projekte (für schnellen Wechsel) + CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + working_dir TEXT NOT NULL, + claude_md_path TEXT, + description TEXT, + last_used TEXT NOT NULL, + created_at TEXT NOT NULL, + session_count INTEGER DEFAULT 0 + ); + CREATE INDEX IF NOT EXISTS idx_projects_last_used ON projects(last_used DESC); + -- Phase 2.0: Fehler-Tracking für Auto-Pattern-Erkennung CREATE TABLE IF NOT EXISTS error_tracker ( error_hash TEXT PRIMARY KEY, @@ -401,7 +414,6 @@ impl Database { } /// Löscht einen Memory-Eintrag - #[allow(dead_code)] pub fn delete_memory_entry(&self, id: &str) -> SqlResult<()> { self.conn.execute("DELETE FROM memory WHERE id = ?1", params![id])?; Ok(()) @@ -1155,3 +1167,123 @@ pub async fn get_error_stats( let db = state.lock().unwrap(); db.get_error_stats(limit).map_err(|e| e.to_string()) } + +// ============ Projekte ============ + +/// Ein Projekt für schnellen Wechsel +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Project { + pub id: String, + pub name: String, + pub working_dir: String, + pub claude_md_path: Option, + pub description: Option, + pub last_used: String, + pub created_at: String, + pub session_count: i64, +} + +impl Database { + /// Alle Projekte laden (zuletzt benutzte zuerst) + pub fn list_projects(&self) -> SqlResult> { + let mut stmt = self.conn.prepare( + "SELECT id, name, working_dir, claude_md_path, description, last_used, created_at, session_count + FROM projects ORDER BY last_used DESC" + )?; + let projects = stmt.query_map([], |row| { + Ok(Project { + id: row.get(0)?, + name: row.get(1)?, + working_dir: row.get(2)?, + claude_md_path: row.get(3)?, + description: row.get(4)?, + last_used: row.get(5)?, + created_at: row.get(6)?, + session_count: row.get(7)?, + }) + })?.collect::>>()?; + Ok(projects) + } + + /// Projekt speichern (INSERT oder UPDATE) + pub fn save_project(&self, project: &Project) -> SqlResult<()> { + self.conn.execute( + "INSERT OR REPLACE INTO projects (id, name, working_dir, claude_md_path, description, last_used, created_at, session_count) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + project.id, project.name, project.working_dir, + project.claude_md_path, project.description, + project.last_used, project.created_at, project.session_count, + ], + )?; + Ok(()) + } + + /// Projekt löschen + pub fn delete_project(&self, id: &str) -> SqlResult<()> { + self.conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?; + Ok(()) + } + + /// Projekt als zuletzt benutzt markieren + pub fn touch_project(&self, id: &str) -> SqlResult<()> { + self.conn.execute( + "UPDATE projects SET last_used = datetime('now'), session_count = session_count + 1 WHERE id = ?1", + params![id], + )?; + Ok(()) + } +} + +// Tauri-Commands für Projekte + +#[tauri::command] +pub async fn list_projects(app: AppHandle) -> Result, String> { + let state = app.state::(); + let db = state.lock().unwrap(); + db.list_projects().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn save_project(app: AppHandle, project: Project) -> Result<(), String> { + let state = app.state::(); + let db = state.lock().unwrap(); + db.save_project(&project).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn delete_project(app: AppHandle, id: String) -> Result<(), String> { + let state = app.state::(); + let db = state.lock().unwrap(); + db.delete_project(&id).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn switch_project(app: AppHandle, project_id: String) -> Result { + let state = app.state::(); + let db = state.lock().unwrap(); + + // Projekt als benutzt markieren + db.touch_project(&project_id).map_err(|e| e.to_string())?; + + // Projekt-Daten laden + let projects = db.list_projects().map_err(|e| e.to_string())?; + let project = projects.into_iter() + .find(|p| p.id == project_id) + .ok_or_else(|| format!("Projekt '{}' nicht gefunden", project_id))?; + + // Sticky-Context aktualisieren: current_project setzen + let project_json = serde_json::json!({ + "id": project.id, + "name": project.name, + "working_dir": project.working_dir, + }).to_string(); + + db.conn.execute( + "INSERT OR REPLACE INTO sticky_context (key, value, priority, updated_at) + VALUES ('project:current', ?1, 10, datetime('now'))", + params![project_json], + ).map_err(|e| e.to_string())?; + + Ok(project) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c53091f..fc5b0c0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -53,6 +53,10 @@ pub fn run() { // Gedächtnis-System memory::load_memory, memory::get_sticky_memory_entries, + memory::save_memory_entry, + memory::delete_memory_entry, + memory::list_memory_entries, + memory::get_autoload_memory, memory::save_pattern, memory::detect_issue, // Audit-Log @@ -97,6 +101,11 @@ pub fn run() { db::track_error, db::set_error_kb_pattern, db::get_error_stats, + // Projekte + db::list_projects, + db::save_project, + db::delete_project, + db::switch_project, // Wissensbasis (claude-db) knowledge::search_knowledge, knowledge::get_knowledge, diff --git a/src-tauri/src/memory.rs b/src-tauri/src/memory.rs index b0b026b..537994b 100644 --- a/src-tauri/src/memory.rs +++ b/src-tauri/src/memory.rs @@ -106,6 +106,43 @@ pub async fn save_pattern(app: AppHandle, pattern: Pattern) -> Result<(), String db_lock.save_pattern(&pattern).map_err(|e| e.to_string()) } +/// Speichert einen Memory-Eintrag (von Frontend oder intern) +#[tauri::command] +pub async fn save_memory_entry(app: AppHandle, entry: MemoryEntry) -> Result<(), String> { + println!("🧠 Speichere Memory: {} (sticky={}, auto_load={})", entry.key, entry.sticky, entry.auto_load); + + let state = app.state::>>(); + let db_lock = state.lock().unwrap(); + db_lock.save_memory_entry(&entry).map_err(|e| e.to_string()) +} + +/// Löscht einen Memory-Eintrag +#[tauri::command] +pub async fn delete_memory_entry(app: AppHandle, id: String) -> Result<(), String> { + let state = app.state::>>(); + let db_lock = state.lock().unwrap(); + db_lock.delete_memory_entry(&id).map_err(|e| e.to_string()) +} + +/// Listet alle Memory-Einträge (für UI) +#[tauri::command] +pub async fn list_memory_entries(app: AppHandle) -> Result, String> { + let state = app.state::>>(); + let db_lock = state.lock().unwrap(); + db_lock.load_memory_entries().map_err(|e| e.to_string()) +} + +/// Holt nur auto_load Einträge (für Session-Start Injection) +#[tauri::command] +pub async fn get_autoload_memory(app: AppHandle) -> Result, String> { + let state = app.state::>>(); + let db_lock = state.lock().unwrap(); + let entries = db_lock.load_memory_entries().map_err(|e| e.to_string())?; + let autoload: Vec = entries.into_iter().filter(|e| e.auto_load).collect(); + println!("🧠 {} Auto-Load Einträge für Session-Start", autoload.len()); + Ok(autoload) +} + /// Erkennt ein Problem und schlägt Korrektur vor #[tauri::command] pub async fn detect_issue( diff --git a/src/lib/components/ChatPanel.svelte b/src/lib/components/ChatPanel.svelte index d45a16a..856f0ab 100644 --- a/src/lib/components/ChatPanel.svelte +++ b/src/lib/components/ChatPanel.svelte @@ -222,6 +222,102 @@ // Quick-Actions Palette (Ctrl+K) let showQuickActions = $state(false); + // File-Drop State + let isDragOver = $state(false); + let dragCounter = 0; // Verhindert Flackern bei verschachtelten Elementen + + function handleDragEnter(e: DragEvent) { + e.preventDefault(); + dragCounter++; + if (e.dataTransfer?.types.includes('Files')) { + isDragOver = true; + } + } + + function handleDragLeave(e: DragEvent) { + e.preventDefault(); + dragCounter--; + if (dragCounter <= 0) { + isDragOver = false; + dragCounter = 0; + } + } + + function handleDragOver(e: DragEvent) { + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; + } + } + + async function handleDrop(e: DragEvent) { + e.preventDefault(); + isDragOver = false; + dragCounter = 0; + + const files = e.dataTransfer?.files; + if (!files || files.length === 0) return; + + const fileContents: string[] = []; + + for (const file of Array.from(files)) { + try { + // Bilder: als Base64 einbetten (Claude kann Bilder analysieren) + if (file.type.startsWith('image/')) { + const base64 = await fileToBase64(file); + fileContents.push(`📎 **${file.name}** (${formatSize(file.size)}, ${file.type})\n\n![${file.name}](${base64})`); + continue; + } + + // Textdateien: Inhalt lesen + if (file.size > 500_000) { + fileContents.push(`📎 **${file.name}** (${formatSize(file.size)}) — Datei zu groß für direkten Chat (max 500KB). Bitte über Dateipfad referenzieren.`); + continue; + } + + const text = await file.text(); + const ext = file.name.split('.').pop() || ''; + const lang = extToLang(ext); + fileContents.push(`📎 **${file.name}** (${formatSize(file.size)})\n\`\`\`${lang}\n${text}\n\`\`\``); + } catch (err) { + fileContents.push(`📎 **${file.name}** — Fehler beim Lesen: ${err}`); + } + } + + if (fileContents.length > 0) { + const plural = files.length > 1 ? `${files.length} Dateien` : 'Datei'; + const prefix = `Ich habe dir ${plural} in den Chat gezogen. Analysiere bitte:\n\n`; + $currentInput = prefix + fileContents.join('\n\n---\n\n'); + inputTextarea?.focus(); + } + } + + function fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + function formatSize(bytes: number): string { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + } + + function extToLang(ext: string): string { + const map: Record = { + ts: 'typescript', js: 'javascript', py: 'python', rs: 'rust', + svelte: 'svelte', html: 'html', css: 'css', json: 'json', + yaml: 'yaml', yml: 'yaml', toml: 'toml', md: 'markdown', + sh: 'bash', sql: 'sql', php: 'php', nix: 'nix', xml: 'xml', + vue: 'vue', tsx: 'tsx', jsx: 'jsx', go: 'go', java: 'java', + }; + return map[ext.toLowerCase()] || ext; + } + // Slash-Command Autocomplete State let showCommandPalette = $state(false); let commandQuery = $state(''); @@ -923,7 +1019,21 @@ } -
+ +
+ + {#if isDragOver} +
+
📎
+

Dateien hier ablegen

+

Code, Text, Bilder — Claude analysiert alles

+
+ {/if} +

💬 Chat

@@ -2195,4 +2305,49 @@ color: var(--text-secondary); margin-top: var(--spacing-sm); } + + /* File-Drop Overlay */ + .chat-panel.drag-over { + outline: 2px dashed var(--accent); + outline-offset: -2px; + } + + .drop-overlay { + position: absolute; + inset: 0; + z-index: 50; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + background: rgba(124, 58, 237, 0.12); + backdrop-filter: blur(4px); + border-radius: inherit; + pointer-events: none; + } + + .drop-icon { + font-size: 3rem; + animation: dropBounce 0.5s ease; + } + + @keyframes dropBounce { + 0% { transform: scale(0.5); opacity: 0; } + 60% { transform: scale(1.2); } + 100% { transform: scale(1); opacity: 1; } + } + + .drop-overlay p { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--accent); + } + + .drop-hint { + font-size: 0.75rem !important; + font-weight: 400 !important; + color: var(--text-secondary) !important; + } diff --git a/src/lib/components/SessionList.svelte b/src/lib/components/SessionList.svelte index 102f943..3b40bc7 100644 --- a/src/lib/components/SessionList.svelte +++ b/src/lib/components/SessionList.svelte @@ -19,12 +19,94 @@ last_message: string | null; } + interface Project { + id: string; + name: string; + working_dir: string; + claude_md_path: string | null; + description: string | null; + last_used: string; + created_at: string; + session_count: number; + } + let sessions: Session[] = []; let activeSessionId: string | null = null; let loading = true; let showNewForm = false; let newTitle = ''; + // Projekt-Wechsel + let projects: Project[] = []; + let activeProject: Project | null = null; + let showProjectAdd = false; + let newProjectName = ''; + let newProjectDir = ''; + let showProjectList = false; + + async function loadProjects() { + try { + projects = await invoke('list_projects'); + console.log(`📁 ${projects.length} Projekte geladen`); + } catch (err) { + console.error('Fehler beim Laden der Projekte:', err); + } + } + + async function addProject() { + const name = newProjectName.trim(); + const dir = newProjectDir.trim(); + if (!name || !dir) return; + + const project: Project = { + id: crypto.randomUUID(), + name, + working_dir: dir, + claude_md_path: null, + description: null, + last_used: new Date().toISOString(), + created_at: new Date().toISOString(), + session_count: 0, + }; + + try { + await invoke('save_project', { project }); + newProjectName = ''; + newProjectDir = ''; + showProjectAdd = false; + await loadProjects(); + // Direkt zum neuen Projekt wechseln + await switchProject(project.id); + } catch (err) { + console.error('Fehler beim Erstellen des Projekts:', err); + } + } + + async function switchProject(projectId: string) { + try { + const project: Project = await invoke('switch_project', { projectId }); + activeProject = project; + showProjectList = false; + console.log(`📂 Projekt gewechselt: ${project.name} → ${project.working_dir}`); + // Sessions neu laden (gefiltert nach Projekt wäre Zukunftsmusik) + await loadSessions(); + } catch (err) { + console.error('Fehler beim Projektwechsel:', err); + } + } + + async function removeProject(id: string) { + try { + await invoke('delete_project', { id }); + if (activeProject?.id === id) { + activeProject = null; + } + await loadProjects(); + } catch (err) { + console.error('Fehler beim Löschen des Projekts:', err); + } + } + async function loadSessions() { try { sessions = await invoke('list_sessions', { limit: 50 }); @@ -55,6 +137,7 @@ let sessionCreatedListener: UnlistenFn | null = null; onMount(async () => { + await loadProjects(); loadSessions(); // Auf Auto-Session-Erstellung vom ChatPanel hören sessionCreatedListener = await listen('session-created', () => { @@ -72,7 +155,7 @@ try { const session: Session = await invoke('create_session', { title, - workingDir: null, + workingDir: activeProject?.working_dir || null, }); activeSessionId = session.id; $currentSessionId = session.id; @@ -129,12 +212,88 @@ if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`; return `${usd.toFixed(2).replace('.', ',')}$`; } + + function shortenPath(path: string): string { + // /mnt/17 - Entwicklungen/20 - Projekte/Foo → .../Foo + const parts = path.split('/'); + if (parts.length > 3) return `.../${parts.slice(-1)[0]}`; + return path; + }
+ +
+ + +
showProjectList = !showProjectList}> + 📁 + {#if activeProject} + {activeProject.name} + {shortenPath(activeProject.working_dir)} + {:else} + Kein Projekt + {/if} + {showProjectList ? '▲' : '▼'} +
+ + {#if showProjectList} +
+ {#each projects as project} + + +
switchProject(project.id)} + > +
+ {project.name} + {shortenPath(project.working_dir)} +
+
+ {project.session_count} + +
+
+ {/each} + + {#if !showProjectAdd} + + {:else} +
+ e.key === 'Enter' && addProject()} + /> + e.key === 'Enter' && addProject()} + /> +
+ + +
+
+ {/if} +
+ {/if} +
+ +

💬 Sessions

-
@@ -145,9 +304,9 @@ type="text" bind:value={newTitle} placeholder="Session-Titel (optional)" - on:keydown={(e) => e.key === 'Enter' && createSession()} + onkeydown={(e) => e.key === 'Enter' && createSession()} /> - +
{/if} @@ -156,7 +315,7 @@ {:else if sessions.length === 0}

Keine Sessions vorhanden.

-
@@ -168,7 +327,7 @@
resumeSession(session.id)} + onclick={() => resumeSession(session.id)} >
@@ -194,7 +353,7 @@ {formatDate(session.updated_at)}