From 71ab5ec8307e590546d8dc1c6d693060b61d342a Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 27 Apr 2026 22:13:52 +0200 Subject: [PATCH] feat: Schulungsmodus Datei-Animationen + Permission-Toggle + Chat-Scroll-Fix [appimage] - AnimatedFileEdit.svelte: neue Komponente fuer animierte Datei-Aenderungen im Praesentation-Fenster - Schulungsmodus: 5-Stufen-Speed-Regler (Lehrer 10cps bis Data-Modus instant+Glow) - Schulungsmodus: Live-Catchup-Button, Auto-Weiter nach Slide-Abschluss - ChatPanel: Permission-Mode-Toggle links vom Textfeld (default/acceptEdits/bypassPermissions) - ApprovalBar: Floating-Card mit blauem Glow, Buttons umbenannt (Anwenden/Ablehnen) - MessageList: Scroll-Guard mit scrollend-Event + 700ms-Fallback statt doppeltem rAF - MessageList: User-Nachrichten scrollen sofort nach unten (requestAnimationFrame + force) - Message.svelte: MessagePart[]-basiertes Rendering fuer chronologische Reihenfolge - events.ts: file-change sendet Slide an Praesentation-Fenster wenn offen - teaching.rs: presentation_send_slide_if_open Command - claude.rs: set/get_permission_mode Commands mit DB-Persistenz Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/claude-bridge.js | 26 ++ src-tauri/src/claude.rs | 44 ++- src-tauri/src/lib.rs | 3 + src-tauri/src/teaching.rs | 22 +- src/lib/components/AnimatedFileEdit.svelte | 298 +++++++++++++++++++++ src/lib/components/ApprovalBar.svelte | 54 ++-- src/lib/components/ChatPanel.svelte | 84 +++++- src/lib/components/Message.svelte | 68 +++-- src/lib/components/MessageList.svelte | 36 ++- src/lib/stores/app.ts | 77 +++++- src/lib/stores/events.ts | 86 ++++-- src/routes/presentation/+page.svelte | 175 ++++++++++-- 12 files changed, 888 insertions(+), 85 deletions(-) create mode 100644 src/lib/components/AnimatedFileEdit.svelte diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 1a59559..4367d1d 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -50,6 +50,12 @@ const CHANGE_TOOLS = ['Edit', 'Write']; // Agent-Modus (solo | handlanger | experten | auto) let agentMode = 'solo'; +// Permission-Modus (default | acceptEdits | bypassPermissions) — Phase 11. +// Wird ueber den Permission-Toggle links vom Textfeld umgeschaltet und an +// queryOptions weitergereicht damit Edits / Bash automatisch akzeptiert +// werden koennen wenn der User das will. +let permissionMode = 'default'; + // Sticky Context (Schicht 1) — wird bei JEDEM API-Call injiziert let stickyContext = ''; @@ -463,6 +469,12 @@ async function sendMessage(message, requestId, model = null, contextOverride = n queryOptions.tools = { type: 'preset', preset: 'claude_code' }; queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob', 'Write', 'Edit', 'Bash']; + // Phase 11: Permission-Modus aus dem Toggle-Knopf links vom Textfeld. + // 'default' nicht setzen — SDK fragt dann ueber den Approval-Hook. + if (permissionMode !== 'default') { + queryOptions.permissionMode = permissionMode; + } + if (effectiveMode === 'handlanger') { sendMonitorEvent('agent', 'Handlanger: Delegation per System-Prompt durchgesetzt', { mode: effectiveMode, @@ -913,6 +925,20 @@ function handleCommand(msg) { sendMonitorEvent('agent', `Agent-Modus geändert: ${agentMode}`, { mode: agentMode }); break; + case 'set-permission-mode': { + // Permission-Modus setzen (default, acceptEdits, bypassPermissions) + const validPerms = ['default', 'acceptEdits', 'bypassPermissions']; + if (!msg.mode || !validPerms.includes(msg.mode)) { + sendError(msg.id, `Ungültiger Permission-Modus: ${msg.mode}. Verfügbar: ${validPerms.join(', ')}`); + return; + } + permissionMode = msg.mode; + sendResponse(msg.id, { mode: permissionMode, status: 'Permission-Modus geändert' }); + sendEvent('permission-mode-changed', { mode: permissionMode }); + sendMonitorEvent('agent', `Permission-Modus geändert: ${permissionMode}`, { mode: permissionMode }); + break; + } + case 'get-mode': sendResponse(msg.id, { mode: agentMode }); break; diff --git a/src-tauri/src/claude.rs b/src-tauri/src/claude.rs index 02dd113..052eb0b 100644 --- a/src-tauri/src/claude.rs +++ b/src-tauri/src/claude.rs @@ -612,7 +612,7 @@ fn send_to_bridge_full( "id": request_id, "model": message }), - "set-mode" => serde_json::json!({ + "set-mode" | "set-permission-mode" => serde_json::json!({ "command": command, "id": request_id, "mode": message @@ -921,6 +921,48 @@ pub async fn get_agent_mode(app: AppHandle) -> Result { Ok("solo".to_string()) } +/// Permission-Modus setzen (default | acceptEdits | bypassPermissions). +/// Wird vom Toggle-Knopf links vom Textfeld aufgerufen. +#[tauri::command] +pub async fn set_permission_mode(app: AppHandle, mode: String) -> Result { + let valid = ["default", "acceptEdits", "bypassPermissions"]; + if !valid.contains(&mode.as_str()) { + return Err(format!("Ungültiger Permission-Modus: {}. Verfügbar: {}", mode, valid.join(", "))); + } + + println!("🔐 Permission-Modus wechseln zu: {}", mode); + + if let Some(db_state) = app.try_state::>>() { + let db = db_state.lock().unwrap(); + let _ = db.set_setting("permission_mode", &mode); + } + + let needs_start = { + let state = app.state::>>(); + let s = state.lock().unwrap(); + !s.is_connected() + }; + if needs_start { + start_bridge(&app)?; + tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; + } + + send_to_bridge(&app, "set-permission-mode", &mode)?; + Ok(mode) +} + +/// Aktuellen Permission-Modus aus Settings laden. +#[tauri::command] +pub async fn get_permission_mode(app: AppHandle) -> Result { + if let Some(db_state) = app.try_state::>>() { + let db = db_state.lock().unwrap(); + if let Ok(Some(mode)) = db.get_setting("permission_mode") { + return Ok(mode); + } + } + Ok("default".to_string()) +} + /// Modell-Info Struct #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ModelInfo { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4108f46..a98be88 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -55,6 +55,8 @@ pub fn run() { claude::get_current_model, claude::set_agent_mode, claude::get_agent_mode, + claude::set_permission_mode, + claude::get_permission_mode, claude::init_sticky_context, claude::get_bridge_status, claude::stop_bridge_daemon, @@ -189,6 +191,7 @@ pub fn run() { teaching::presentation_open, teaching::presentation_close, teaching::presentation_send_slide, + teaching::presentation_send_slide_if_open, teaching::presentation_clear, // Chat-Fenster (herauslösen) chat_window::chat_window_open, diff --git a/src-tauri/src/teaching.rs b/src-tauri/src/teaching.rs index 7efb73e..068c1fb 100644 --- a/src-tauri/src/teaching.rs +++ b/src-tauri/src/teaching.rs @@ -6,12 +6,21 @@ use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Slide { - pub r#type: String, // "mermaid" | "code" | "text" + pub r#type: String, // "mermaid" | "code" | "text" | "file-edit" + #[serde(default, skip_serializing_if = "String::is_empty")] pub content: String, #[serde(skip_serializing_if = "Option::is_none")] pub language: Option, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, + // file-edit Felder (Phase 11): Datei-Aenderungen werden in der + // Praesentation animiert nachgespielt. + #[serde(skip_serializing_if = "Option::is_none")] + pub file_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub before: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, } #[tauri::command] @@ -62,3 +71,14 @@ pub async fn presentation_clear(app: AppHandle) -> Result<(), String> { } Ok(()) } + +/// Schickt eine Slide nur wenn das Praesentations-Fenster bereits offen ist. +/// Wird vom file-change-Handler aufgerufen — wenn der User den Schulungsmodus +/// nicht aktiv hat soll natuerlich nichts passieren. +#[tauri::command] +pub async fn presentation_send_slide_if_open(app: AppHandle, slide: Slide) -> Result<(), String> { + if let Some(win) = app.get_webview_window("presentation") { + win.emit("presentation-slide", &slide).map_err(|e| e.to_string())?; + } + Ok(()) +} diff --git a/src/lib/components/AnimatedFileEdit.svelte b/src/lib/components/AnimatedFileEdit.svelte new file mode 100644 index 0000000..2a9e63c --- /dev/null +++ b/src/lib/components/AnimatedFileEdit.svelte @@ -0,0 +1,298 @@ + + +
200}> +
+ + + + {filePath} + {language} +
+
{#each ops as op, i (i)}{#if op.kind === 'keep'}{op.text}
+{:else if op.kind === 'del'}{op.text}
+{:else if op.kind === 'add'}{visibleAddText(i, op.text)}{#if i === currentAddIdx}{/if}
+{/if}{/each}
+
+ + diff --git a/src/lib/components/ApprovalBar.svelte b/src/lib/components/ApprovalBar.svelte index aa04f27..a12fb69 100644 --- a/src/lib/components/ApprovalBar.svelte +++ b/src/lib/components/ApprovalBar.svelte @@ -32,7 +32,7 @@ await invoke('accept_change', { toolId }); pendingChanges.update((list) => list.filter((c) => c.toolId !== toolId)); } catch (err) { - addMessage('system', `Fehler beim Übernehmen: ${err}`); + addMessage('system', `Fehler beim Anwenden: ${err}`); } finally { busy = false; } @@ -46,7 +46,7 @@ pendingChanges.update((list) => list.filter((c) => c.toolId !== toolId)); addMessage('system', `↩️ ${result}`); } catch (err) { - addMessage('system', `Fehler beim Verwerfen: ${err}`); + addMessage('system', `Fehler beim Ablehnen: ${err}`); } finally { busy = false; } @@ -148,7 +148,7 @@ disabled={busy} title="Änderung verwerfen — Datei bleibt unverändert (Ctrl+Backspace)" > - ✕ Verwerfen + ✕ Ablehnen {:else} @@ -209,7 +209,7 @@ type="button" onclick={() => accept(change.toolId)} disabled={busy} - title="Übernehmen" + title="Anwenden" > ✓ @@ -222,20 +222,44 @@ {/if}