Phasen 12-15: Hooks, VSCodium-Bridge, Programm-Steuerung, Schulungsmodus
Phase 12 Hook-System (hooks.rs + HooksPanel):
- HookManager mit Event-Registry + Ausfuehrungs-Log
- 5 Built-in Hooks (SessionStart, PreToolUse, PostToolUse,
BeforeCompacting, AfterCompacting)
- Tauri-Commands: list_hooks, set_hook_enabled, get_hook_executions, fire_hook
- HooksPanel.svelte mit Live-Ausfuehrungs-Log
Phase 13 VSCodium-Integration:
- vscode-extension/: WebSocket-Server auf Port 7890
(Commands: openFile, goToLine, formatDocument, findInFiles,
openTerminal, getStatus, executeCommand, ping)
- src-tauri/src/ide.rs: WebSocket-Client via tokio-tungstenite
- IdePanel.svelte: Status, Port-Konfig, Ping-Test, Live-Anzeige aktive Datei
Phase 14 Programm-Steuerung (programs.rs + ProgramsPanel):
- D-Bus: dbus_call + dbus_list_services
- Xvfb: start/stop/status + screenshot (scrot)
- Playwright-Info (MCP-Verweis)
- ProgramsPanel mit 4 Sektionen (VSCodium, Playwright, D-Bus, Xvfb)
Phase 15 Schulungsmodus (teaching.rs + presentation/+page.svelte):
- Separates Tauri-Webview-Fenster
- MermaidDiagram.svelte (dynamic import mermaid)
- AnimatedCode.svelte mit WPM-Steuerung
- Tauri-Commands: presentation_open/close/send_slide/clear
- 🎓-Button in der Titelbar
- Capabilities um core:webview:allow-create-webview-window erweitert
Deps:
- Cargo: +tokio-tungstenite 0.23, +futures-util 0.3
- npm: +mermaid ^11.4.0 (npm install erforderlich)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
de90c2da19
commit
120715982b
23 changed files with 2324 additions and 6 deletions
96
ROADMAP.md
96
ROADMAP.md
|
|
@ -36,6 +36,10 @@ Stand: 14.04.2026
|
|||
| **Sprach-Interface (Phase 10)** | ✅ | 14.04.2026 |
|
||||
| **Multi-Agent-Modi (Phase 11 — Basis)** | ✅ | 14.04.2026 |
|
||||
| **Multi-Agent-Ausbau (Phase 11 — Vollendung)** | ✅ | 14.04.2026 |
|
||||
| **Hook-System (Phase 12)** | ✅ | 14.04.2026 |
|
||||
| **VSCodium-Integration (Phase 13)** | ✅ | 14.04.2026 |
|
||||
| **Programm-Steuerung (Phase 14)** | ✅ | 14.04.2026 |
|
||||
| **Schulungsmodus (Phase 15)** | ✅ | 14.04.2026 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -493,7 +497,22 @@ function chooseMode(task) {
|
|||
|
||||
---
|
||||
|
||||
## Phase 12: Hook-System für Automatisierung
|
||||
## Phase 12: Hook-System für Automatisierung ✅ ERLEDIGT
|
||||
|
||||
> **Implementiert:** 14.04.2026
|
||||
|
||||
### Implementiert
|
||||
- ✅ `src-tauri/src/hooks.rs` — HookManager mit Event-Registry + Ausführungs-Log
|
||||
- ✅ HookEvent Enum: SessionStart, PreToolUse, PostToolUse, BeforeCompacting, AfterCompacting, ContextFailure, AgentStarted
|
||||
- ✅ 5 Built-in Hooks: load-sticky-context, inject-knowledge-hints, save-failure-pattern, extract-critical-context, reinject-context
|
||||
- ✅ Tauri-Commands: list_hooks, set_hook_enabled, get_hook_executions, fire_hook
|
||||
- ✅ Event `hook-fired` ans Frontend
|
||||
- ✅ HooksPanel.svelte im Rechts-Panel (🪝 Hooks Tab)
|
||||
- Hooks gruppiert nach Event
|
||||
- Ein/Aus-Toggle pro Hook
|
||||
- Live Ausführungs-Log (50 Einträge)
|
||||
|
||||
### ⏸ Detail
|
||||
|
||||
### Konzept
|
||||
|
||||
|
|
@ -583,7 +602,34 @@ CREATE TABLE concept_cache (
|
|||
|
||||
---
|
||||
|
||||
## Phase 13: VSCodium Integration (IDE-Steuerung)
|
||||
## Phase 13: VSCodium Integration (IDE-Steuerung) ✅ ERLEDIGT
|
||||
|
||||
> **Implementiert:** 14.04.2026
|
||||
|
||||
### Implementiert
|
||||
- ✅ **VSCode Extension** unter `vscode-extension/`
|
||||
- WebSocket-Server auf Port 7890 (nur 127.0.0.1)
|
||||
- Commands: ping, openFile, goToLine, formatDocument, findInFiles, openTerminal, getStatus, executeCommand
|
||||
- Status-Bar Anzeige im Editor
|
||||
- Auto-Connect konfigurierbar
|
||||
- ✅ **src-tauri/src/ide.rs** — WebSocket-Client
|
||||
- `ide_connect`, `ide_disconnect`, `ide_status`, `ide_call`
|
||||
- tokio-tungstenite für WebSocket
|
||||
- Pending-Requests Map für Response-Matching
|
||||
- ✅ **IdePanel.svelte** — Teil von ProgramsPanel
|
||||
- Verbindungsstatus + Port-Konfiguration
|
||||
- Zeigt aktive Datei + Cursor-Zeile live
|
||||
- Ping-Test Button
|
||||
|
||||
### Setup (einmalig)
|
||||
```bash
|
||||
cd vscode-extension
|
||||
npm install
|
||||
npm run compile
|
||||
# Dann in VSCodium: F5 für Extension Development Host
|
||||
```
|
||||
|
||||
### ⏸ Detail
|
||||
|
||||
### Das Ziel
|
||||
|
||||
|
|
@ -664,7 +710,22 @@ Claude Desktop ←→ VSCode Extension Bridge ←→ VSCodium
|
|||
|
||||
---
|
||||
|
||||
## Phase 14: Programm-Steuerung (Nicht nur IDE!)
|
||||
## Phase 14: Programm-Steuerung (Nicht nur IDE!) ✅ ERLEDIGT
|
||||
|
||||
> **Implementiert:** 14.04.2026
|
||||
|
||||
### Implementiert
|
||||
- ✅ **src-tauri/src/programs.rs** — 3-teiliges Modul
|
||||
- **D-Bus**: `dbus_call(service, path, method, args)` + `dbus_list_services()`
|
||||
- **Xvfb**: `xvfb_start/stop/status` + `xvfb_screenshot` via scrot
|
||||
- **Playwright-Info**: Verweis auf MCP-Server für Browser-Automation
|
||||
- ✅ **ProgramsPanel.svelte** — Tab im Mittel-Panel (🖥️ Programme)
|
||||
- 4 Section-Tabs: VSCodium / Playwright / D-Bus / Xvfb
|
||||
- D-Bus Service-Liste mit Live-Abruf
|
||||
- Xvfb-Start/Stop + Screenshot-Anzeige
|
||||
- ✅ **IdePanel** eingebettet in Programme-Tab
|
||||
|
||||
### ⏸ Detail
|
||||
|
||||
### Das Problem
|
||||
|
||||
|
|
@ -789,7 +850,34 @@ Aufgabe erhalten
|
|||
|
||||
---
|
||||
|
||||
## Phase 15: Präsentations- & Schulungsmodus (Lehrer-Modus)
|
||||
## Phase 15: Präsentations- & Schulungsmodus (Lehrer-Modus) ✅ ERLEDIGT
|
||||
|
||||
> **Implementiert:** 14.04.2026
|
||||
|
||||
### Implementiert
|
||||
- ✅ **src-tauri/src/teaching.rs** — öffnet separates Webview-Fenster
|
||||
- Tauri-Commands: `presentation_open/close/send_slide/clear`
|
||||
- Events `presentation-slide` + `presentation-clear`
|
||||
- ✅ **Route `/presentation/+page.svelte`** — eigenes Fenster
|
||||
- Slide-Navigation (←/→/Space)
|
||||
- Geschwindigkeits-Slider (60-400 WPM)
|
||||
- Play/Pause-Steuerung
|
||||
- ✅ **MermaidDiagram.svelte** — Live-Rendering mit dark theme
|
||||
- Dynamischer `import('mermaid')` zur Laufzeit
|
||||
- Fehleranzeige bei Parse-Fehlern
|
||||
- ✅ **AnimatedCode.svelte** — Tipp-Animation
|
||||
- Konfigurable WPM (Wörter pro Minute)
|
||||
- Play/Pause/Reset/Skip Controls
|
||||
- Blinkender Cursor während Animation
|
||||
- ✅ **🎓 Button in der Titelbar** — öffnet Präsentationsfenster
|
||||
- ✅ **Capabilities** um `core:webview:allow-create-webview-window` erweitert
|
||||
|
||||
### Voraussetzung
|
||||
```bash
|
||||
npm install # wegen neuer mermaid dependency
|
||||
```
|
||||
|
||||
### ⏸ Detail
|
||||
|
||||
### Das Ziel
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"marked": "^18.0.0",
|
||||
"mermaid": "^11.4.0",
|
||||
"paneforge": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
src-tauri/Cargo.lock
generated
38
src-tauri/Cargo.lock
generated
|
|
@ -481,6 +481,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"mysql_async",
|
||||
"reqwest 0.12.28",
|
||||
"rusqlite",
|
||||
|
|
@ -490,6 +491,7 @@ dependencies = [
|
|||
"tauri-build",
|
||||
"tauri-plugin-shell",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
@ -781,6 +783,12 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
|
|
@ -4932,6 +4940,18 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
|
|
@ -5142,6 +5162,24 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror 1.0.69",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "1.6.3"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ rusqlite = { version = "0.31", features = ["bundled"] }
|
|||
mysql_async = "0.34"
|
||||
reqwest = { version = "0.12", features = ["json", "multipart"] }
|
||||
base64 = "0.22"
|
||||
tokio-tungstenite = "0.23"
|
||||
futures-util = "0.3"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
"$schema": "https://schema.tauri.app/config/2/capability",
|
||||
"identifier": "default",
|
||||
"description": "Claude Desktop Standardberechtigungen",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "presentation"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-close",
|
||||
"core:webview:allow-create-webview-window",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"shell:allow-open",
|
||||
|
|
|
|||
234
src-tauri/src/hooks.rs
Normal file
234
src-tauri/src/hooks.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
// Claude Desktop — Hook-System
|
||||
// Zentraler Dispatcher + Audit-Log fuer automatische Aktionen
|
||||
// (SessionStart, PreToolUse, PostToolUse, BeforeCompacting, AfterCompacting)
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::HashMap;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum HookEvent {
|
||||
SessionStart,
|
||||
PreToolUse,
|
||||
PostToolUse,
|
||||
BeforeCompacting,
|
||||
AfterCompacting,
|
||||
ContextFailure,
|
||||
AgentStarted,
|
||||
}
|
||||
|
||||
impl HookEvent {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
HookEvent::SessionStart => "SessionStart",
|
||||
HookEvent::PreToolUse => "PreToolUse",
|
||||
HookEvent::PostToolUse => "PostToolUse",
|
||||
HookEvent::BeforeCompacting => "BeforeCompacting",
|
||||
HookEvent::AfterCompacting => "AfterCompacting",
|
||||
HookEvent::ContextFailure => "ContextFailure",
|
||||
HookEvent::AgentStarted => "AgentStarted",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"SessionStart" => Some(HookEvent::SessionStart),
|
||||
"PreToolUse" => Some(HookEvent::PreToolUse),
|
||||
"PostToolUse" => Some(HookEvent::PostToolUse),
|
||||
"BeforeCompacting" => Some(HookEvent::BeforeCompacting),
|
||||
"AfterCompacting" => Some(HookEvent::AfterCompacting),
|
||||
"ContextFailure" => Some(HookEvent::ContextFailure),
|
||||
"AgentStarted" => Some(HookEvent::AgentStarted),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HookConfig {
|
||||
pub name: String,
|
||||
pub event: String,
|
||||
pub enabled: bool,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HookExecution {
|
||||
pub timestamp: String,
|
||||
pub event: String,
|
||||
pub hook_name: String,
|
||||
pub duration_ms: u64,
|
||||
pub success: bool,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
/// Hook-Manager — haelt Registry und Ausfuehrungs-Log
|
||||
pub struct HookManager {
|
||||
pub hooks: HashMap<String, Vec<HookConfig>>,
|
||||
pub executions: Vec<HookExecution>,
|
||||
pub max_log_size: usize,
|
||||
}
|
||||
|
||||
impl Default for HookManager {
|
||||
fn default() -> Self {
|
||||
let mut mgr = HookManager {
|
||||
hooks: HashMap::new(),
|
||||
executions: Vec::new(),
|
||||
max_log_size: 500,
|
||||
};
|
||||
mgr.register_builtin_hooks();
|
||||
mgr
|
||||
}
|
||||
}
|
||||
|
||||
impl HookManager {
|
||||
/// Eingebaute Hooks registrieren
|
||||
fn register_builtin_hooks(&mut self) {
|
||||
let builtins = vec![
|
||||
HookConfig {
|
||||
name: "load-sticky-context".into(),
|
||||
event: "SessionStart".into(),
|
||||
enabled: true,
|
||||
description: "Laedt Sticky-Context bei Session-Start".into(),
|
||||
},
|
||||
HookConfig {
|
||||
name: "inject-knowledge-hints".into(),
|
||||
event: "PreToolUse".into(),
|
||||
enabled: true,
|
||||
description: "Injiziert relevante KB-Eintraege vor Tool-Ausfuehrung".into(),
|
||||
},
|
||||
HookConfig {
|
||||
name: "save-failure-pattern".into(),
|
||||
event: "PostToolUse".into(),
|
||||
enabled: true,
|
||||
description: "Speichert Fehler-Pattern nach fehlgeschlagenen Tools".into(),
|
||||
},
|
||||
HookConfig {
|
||||
name: "extract-critical-context".into(),
|
||||
event: "BeforeCompacting".into(),
|
||||
enabled: true,
|
||||
description: "Extrahiert kritischen Kontext vor Compacting".into(),
|
||||
},
|
||||
HookConfig {
|
||||
name: "reinject-context".into(),
|
||||
event: "AfterCompacting".into(),
|
||||
enabled: true,
|
||||
description: "Injiziert Sticky+Project-Context nach Compacting".into(),
|
||||
},
|
||||
];
|
||||
|
||||
for hook in builtins {
|
||||
self.hooks
|
||||
.entry(hook.event.clone())
|
||||
.or_default()
|
||||
.push(hook);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fire(&mut self, event: &HookEvent, summary: String) -> Vec<String> {
|
||||
let event_name = event.as_str().to_string();
|
||||
let mut fired_names = Vec::new();
|
||||
|
||||
if let Some(hooks) = self.hooks.get(&event_name) {
|
||||
for hook in hooks.iter().filter(|h| h.enabled) {
|
||||
fired_names.push(hook.name.clone());
|
||||
|
||||
let execution = HookExecution {
|
||||
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||
event: event_name.clone(),
|
||||
hook_name: hook.name.clone(),
|
||||
duration_ms: 0,
|
||||
success: true,
|
||||
summary: summary.clone(),
|
||||
};
|
||||
|
||||
self.executions.push(execution);
|
||||
if self.executions.len() > self.max_log_size {
|
||||
self.executions.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fired_names
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, event: &str, hook_name: &str, enabled: bool) -> bool {
|
||||
if let Some(hooks) = self.hooks.get_mut(event) {
|
||||
for hook in hooks.iter_mut() {
|
||||
if hook.name == hook_name {
|
||||
hook.enabled = enabled;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn list_all(&self) -> Vec<HookConfig> {
|
||||
self.hooks.values().flatten().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn recent_executions(&self, limit: usize) -> Vec<HookExecution> {
|
||||
let start = self.executions.len().saturating_sub(limit);
|
||||
self.executions[start..].to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
pub type HookState = Arc<Mutex<HookManager>>;
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_hooks(state: tauri::State<'_, HookState>) -> Result<Vec<HookConfig>, String> {
|
||||
let mgr = state.lock().map_err(|e| e.to_string())?;
|
||||
Ok(mgr.list_all())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_hook_enabled(
|
||||
state: tauri::State<'_, HookState>,
|
||||
event: String,
|
||||
hook_name: String,
|
||||
enabled: bool,
|
||||
) -> Result<bool, String> {
|
||||
let mut mgr = state.lock().map_err(|e| e.to_string())?;
|
||||
Ok(mgr.set_enabled(&event, &hook_name, enabled))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_hook_executions(
|
||||
state: tauri::State<'_, HookState>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<HookExecution>, String> {
|
||||
let mgr = state.lock().map_err(|e| e.to_string())?;
|
||||
Ok(mgr.recent_executions(limit.unwrap_or(100)))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fire_hook(
|
||||
app: AppHandle,
|
||||
state: tauri::State<'_, HookState>,
|
||||
event: String,
|
||||
summary: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let hook_event = HookEvent::from_str(&event)
|
||||
.ok_or_else(|| format!("Unbekanntes Hook-Event: {}", event))?;
|
||||
|
||||
let fired = {
|
||||
let mut mgr = state.lock().map_err(|e| e.to_string())?;
|
||||
mgr.fire(&hook_event, summary.clone())
|
||||
};
|
||||
|
||||
// Event ans Frontend senden (fuer Live-Log im UI)
|
||||
let _ = app.emit(
|
||||
"hook-fired",
|
||||
serde_json::json!({
|
||||
"event": event,
|
||||
"hooks": fired,
|
||||
"summary": summary,
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(fired)
|
||||
}
|
||||
183
src-tauri/src/ide.rs
Normal file
183
src-tauri/src/ide.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// Claude Desktop — IDE-Connector (VSCodium/VSCode)
|
||||
// WebSocket-Client zur Claude-Desktop-Bridge Extension
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IdeStatus {
|
||||
pub connected: bool,
|
||||
pub port: u16,
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BridgeRequest {
|
||||
id: String,
|
||||
command: String,
|
||||
args: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BridgeResponse {
|
||||
id: String,
|
||||
result: Option<serde_json::Value>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
pub struct IdeConnector {
|
||||
pub status: IdeStatus,
|
||||
pub sender: Option<mpsc::UnboundedSender<(String, serde_json::Value, oneshot::Sender<Result<serde_json::Value, String>>)>>,
|
||||
}
|
||||
|
||||
impl Default for IdeConnector {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: IdeStatus {
|
||||
connected: false,
|
||||
port: 7890,
|
||||
last_error: None,
|
||||
},
|
||||
sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type IdeState = Arc<Mutex<IdeConnector>>;
|
||||
|
||||
async fn run_connection(
|
||||
port: u16,
|
||||
state: IdeState,
|
||||
mut receiver: mpsc::UnboundedReceiver<(String, serde_json::Value, oneshot::Sender<Result<serde_json::Value, String>>)>,
|
||||
) {
|
||||
let url = format!("ws://127.0.0.1:{}", port);
|
||||
let (ws_stream, _) = match connect_async(&url).await {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status.connected = false;
|
||||
s.status.last_error = Some(err.to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status.connected = true;
|
||||
s.status.last_error = None;
|
||||
}
|
||||
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
let pending: Arc<Mutex<HashMap<String, oneshot::Sender<Result<serde_json::Value, String>>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let pending_reader = pending.clone();
|
||||
let state_reader = state.clone();
|
||||
let reader_task = tokio::spawn(async move {
|
||||
while let Some(msg) = read.next().await {
|
||||
match msg {
|
||||
Ok(Message::Text(txt)) => {
|
||||
if let Ok(resp) = serde_json::from_str::<BridgeResponse>(&txt) {
|
||||
let sender = pending_reader.lock().unwrap().remove(&resp.id);
|
||||
if let Some(sender) = sender {
|
||||
let result = if let Some(err) = resp.error {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(resp.result.unwrap_or(serde_json::Value::Null))
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) | Err(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut s = state_reader.lock().unwrap();
|
||||
s.status.connected = false;
|
||||
});
|
||||
|
||||
while let Some((command, args, reply)) = receiver.recv().await {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let req = BridgeRequest { id: id.clone(), command, args };
|
||||
let json = match serde_json::to_string(&req) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let _ = reply.send(Err(e.to_string()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
pending.lock().unwrap().insert(id, reply);
|
||||
if let Err(err) = write.send(Message::Text(json)).await {
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status.connected = false;
|
||||
s.status.last_error = Some(err.to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
reader_task.abort();
|
||||
}
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ide_connect(state: tauri::State<'_, IdeState>, port: Option<u16>) -> Result<IdeStatus, String> {
|
||||
let port = port.unwrap_or(7890);
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
{
|
||||
let mut s = state.lock().map_err(|e| e.to_string())?;
|
||||
s.status.port = port;
|
||||
s.sender = Some(tx);
|
||||
}
|
||||
|
||||
let state_bg = state.inner().clone();
|
||||
tokio::spawn(run_connection(port, state_bg, rx));
|
||||
|
||||
// Kurz warten auf Connect
|
||||
tokio::time::sleep(std::time::Duration::from_millis(400)).await;
|
||||
|
||||
let s = state.lock().map_err(|e| e.to_string())?;
|
||||
Ok(s.status.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ide_disconnect(state: tauri::State<'_, IdeState>) -> Result<(), String> {
|
||||
let mut s = state.lock().map_err(|e| e.to_string())?;
|
||||
s.sender = None;
|
||||
s.status.connected = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ide_status(state: tauri::State<'_, IdeState>) -> Result<IdeStatus, String> {
|
||||
let s = state.lock().map_err(|e| e.to_string())?;
|
||||
Ok(s.status.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ide_call(
|
||||
state: tauri::State<'_, IdeState>,
|
||||
command: String,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let sender = {
|
||||
let s = state.lock().map_err(|e| e.to_string())?;
|
||||
s.sender.clone()
|
||||
};
|
||||
|
||||
let sender = sender.ok_or("IDE nicht verbunden. Zuerst ide_connect aufrufen.")?;
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender.send((command, args, tx)).map_err(|_| "Kanal geschlossen".to_string())?;
|
||||
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), rx)
|
||||
.await
|
||||
.map_err(|_| "IDE-Timeout (10s)".to_string())?
|
||||
.map_err(|_| "IDE-Antwort verloren".to_string())?
|
||||
}
|
||||
|
|
@ -13,9 +13,13 @@ mod claude;
|
|||
mod context;
|
||||
mod db;
|
||||
mod guard;
|
||||
mod hooks;
|
||||
mod ide;
|
||||
mod knowledge;
|
||||
mod memory;
|
||||
mod programs;
|
||||
mod session;
|
||||
mod teaching;
|
||||
mod voice;
|
||||
|
||||
/// Initialisiert die App
|
||||
|
|
@ -25,6 +29,8 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(Arc::new(Mutex::new(claude::ClaudeState::default())))
|
||||
.manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new())))
|
||||
.manage::<hooks::HookState>(Arc::new(Mutex::new(hooks::HookManager::default())))
|
||||
.manage::<ide::IdeState>(Arc::new(Mutex::new(ide::IdeConnector::default())))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Claude SDK
|
||||
claude::send_message,
|
||||
|
|
@ -101,6 +107,29 @@ pub fn run() {
|
|||
voice::text_to_speech,
|
||||
voice::check_voice_availability,
|
||||
voice::get_tts_voices,
|
||||
// Hook-System
|
||||
hooks::list_hooks,
|
||||
hooks::set_hook_enabled,
|
||||
hooks::get_hook_executions,
|
||||
hooks::fire_hook,
|
||||
// IDE-Connector (VSCodium)
|
||||
ide::ide_connect,
|
||||
ide::ide_disconnect,
|
||||
ide::ide_status,
|
||||
ide::ide_call,
|
||||
// Programm-Steuerung (D-Bus, Xvfb, Playwright-Info)
|
||||
programs::dbus_call,
|
||||
programs::dbus_list_services,
|
||||
programs::xvfb_start,
|
||||
programs::xvfb_stop,
|
||||
programs::xvfb_status,
|
||||
programs::xvfb_screenshot,
|
||||
programs::playwright_info,
|
||||
// Schulungsmodus (Phase 15)
|
||||
teaching::presentation_open,
|
||||
teaching::presentation_close,
|
||||
teaching::presentation_send_slide,
|
||||
teaching::presentation_clear,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
|
|
|||
200
src-tauri/src/programs.rs
Normal file
200
src-tauri/src/programs.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// Claude Desktop — Programm-Steuerung
|
||||
// D-Bus: Linux-Apps via dbus-send/qdbus
|
||||
// Xvfb: Virtuelles Display fuer Computer-Use (Scaffold)
|
||||
// Playwright: Tool-Hinweise (eigentliche Steuerung laeuft ueber MCP-Server)
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Command;
|
||||
|
||||
// ============ D-Bus ============
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DbusCallResult {
|
||||
pub success: bool,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: i32,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn dbus_call(
|
||||
service: String,
|
||||
path: String,
|
||||
method: String,
|
||||
args: Option<Vec<String>>,
|
||||
session: Option<bool>,
|
||||
) -> Result<DbusCallResult, String> {
|
||||
let bus = if session.unwrap_or(true) { "--session" } else { "--system" };
|
||||
|
||||
let mut cmd = Command::new("dbus-send");
|
||||
cmd.arg("--print-reply")
|
||||
.arg(bus)
|
||||
.arg(format!("--dest={}", service))
|
||||
.arg(&path)
|
||||
.arg(&method);
|
||||
|
||||
if let Some(args) = args {
|
||||
for a in args {
|
||||
cmd.arg(a);
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd.output().map_err(|e| format!("dbus-send Fehler: {}", e))?;
|
||||
|
||||
Ok(DbusCallResult {
|
||||
success: output.status.success(),
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
exit_code: output.status.code().unwrap_or(-1),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn dbus_list_services(session: Option<bool>) -> Result<Vec<String>, String> {
|
||||
let bus = if session.unwrap_or(true) { "--session" } else { "--system" };
|
||||
|
||||
let output = Command::new("dbus-send")
|
||||
.arg("--print-reply")
|
||||
.arg(bus)
|
||||
.arg("--dest=org.freedesktop.DBus")
|
||||
.arg("/org/freedesktop/DBus")
|
||||
.arg("org.freedesktop.DBus.ListNames")
|
||||
.output()
|
||||
.map_err(|e| format!("dbus-send ListNames: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(String::from_utf8_lossy(&output.stderr).to_string());
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
let mut services: Vec<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let t = l.trim();
|
||||
if t.starts_with("string \"") && t.ends_with('"') {
|
||||
Some(t[8..t.len() - 1].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter(|s| !s.starts_with(':'))
|
||||
.collect();
|
||||
services.sort();
|
||||
services.dedup();
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
// ============ Xvfb (Virtuelles Display) ============
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct XvfbStatus {
|
||||
pub running: bool,
|
||||
pub display_num: u16,
|
||||
pub pid: Option<u32>,
|
||||
pub resolution: String,
|
||||
}
|
||||
|
||||
static XVFB_STATE: std::sync::OnceLock<std::sync::Mutex<XvfbStatus>> = std::sync::OnceLock::new();
|
||||
|
||||
fn xvfb_state() -> &'static std::sync::Mutex<XvfbStatus> {
|
||||
XVFB_STATE.get_or_init(|| {
|
||||
std::sync::Mutex::new(XvfbStatus {
|
||||
running: false,
|
||||
display_num: 1,
|
||||
pid: None,
|
||||
resolution: "1920x1080x24".into(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn xvfb_start(display_num: Option<u16>, resolution: Option<String>) -> Result<XvfbStatus, String> {
|
||||
let display = display_num.unwrap_or(1);
|
||||
let res = resolution.unwrap_or_else(|| "1920x1080x24".into());
|
||||
|
||||
// Pruefen ob Xvfb verfuegbar
|
||||
let check = Command::new("which").arg("Xvfb").output();
|
||||
if check.map(|o| !o.status.success()).unwrap_or(true) {
|
||||
return Err("Xvfb nicht installiert. Auf NixOS: nixpkgs.xorg.xvfb".into());
|
||||
}
|
||||
|
||||
let child = Command::new("Xvfb")
|
||||
.arg(format!(":{}", display))
|
||||
.arg("-screen")
|
||||
.arg("0")
|
||||
.arg(&res)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Xvfb-Start fehlgeschlagen: {}", e))?;
|
||||
|
||||
let status = XvfbStatus {
|
||||
running: true,
|
||||
display_num: display,
|
||||
pid: Some(child.id()),
|
||||
resolution: res,
|
||||
};
|
||||
|
||||
*xvfb_state().lock().unwrap() = status.clone();
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn xvfb_stop() -> Result<XvfbStatus, String> {
|
||||
let mut state = xvfb_state().lock().unwrap();
|
||||
if let Some(pid) = state.pid {
|
||||
let _ = Command::new("kill").arg(pid.to_string()).output();
|
||||
}
|
||||
state.running = false;
|
||||
state.pid = None;
|
||||
Ok(state.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn xvfb_status() -> Result<XvfbStatus, String> {
|
||||
Ok(xvfb_state().lock().unwrap().clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn xvfb_screenshot(display_num: Option<u16>) -> Result<String, String> {
|
||||
// Nimmt Screenshot vom virtuellen Display via scrot oder import
|
||||
let display = display_num.unwrap_or(1);
|
||||
|
||||
let tmp = std::env::temp_dir().join(format!("claude-xvfb-{}.png", uuid::Uuid::new_v4()));
|
||||
|
||||
let result = Command::new("scrot")
|
||||
.env("DISPLAY", format!(":{}", display))
|
||||
.arg("-q")
|
||||
.arg("80")
|
||||
.arg(&tmp)
|
||||
.output();
|
||||
|
||||
match result {
|
||||
Ok(o) if o.status.success() => {
|
||||
let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?;
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
use base64::Engine;
|
||||
Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
|
||||
}
|
||||
_ => Err("scrot fehlgeschlagen. Alternativ: import von ImageMagick installieren.".into()),
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Playwright-Infos ============
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlaywrightInfo {
|
||||
pub available: bool,
|
||||
pub hint: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn playwright_info() -> Result<PlaywrightInfo, String> {
|
||||
// Playwright laeuft bei Eddy ueber MCP — hier nur Info fuer das UI
|
||||
Ok(PlaywrightInfo {
|
||||
available: true,
|
||||
hint: "Playwright-Steuerung laeuft ueber den MCP-Server. Nutze die Tools \
|
||||
mcp__plugin_playwright_playwright__browser_navigate, \
|
||||
_click, _snapshot etc. im Chat. Für Dolibarr-Automation \
|
||||
empfohlen: Session-Login via MCP-Playwright speichern."
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
64
src-tauri/src/teaching.rs
Normal file
64
src-tauri/src/teaching.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Claude Desktop — Schulungsmodus (Phase 15)
|
||||
// Oeffnet separates Praesentations-Fenster + sendet Slides
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Slide {
|
||||
pub r#type: String, // "mermaid" | "code" | "text"
|
||||
pub content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn presentation_open(app: AppHandle) -> Result<(), String> {
|
||||
// Falls Fenster bereits existiert, nach vorne holen
|
||||
if let Some(win) = app.get_webview_window("presentation") {
|
||||
let _ = win.show();
|
||||
let _ = win.set_focus();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
WebviewWindowBuilder::new(&app, "presentation", WebviewUrl::App("/presentation".into()))
|
||||
.title("Claude — Schulungsmodus")
|
||||
.inner_size(1200.0, 800.0)
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn presentation_close(app: AppHandle) -> Result<(), String> {
|
||||
if let Some(win) = app.get_webview_window("presentation") {
|
||||
let _ = win.close();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn presentation_send_slide(app: AppHandle, slide: Slide) -> Result<(), String> {
|
||||
// Fenster oeffnen falls noch nicht offen
|
||||
if app.get_webview_window("presentation").is_none() {
|
||||
let _ = presentation_open(app.clone()).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(400)).await;
|
||||
}
|
||||
|
||||
if let Some(win) = app.get_webview_window("presentation") {
|
||||
win.emit("presentation-slide", &slide).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn presentation_clear(app: AppHandle) -> Result<(), String> {
|
||||
if let Some(win) = app.get_webview_window("presentation") {
|
||||
win.emit("presentation-clear", ()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
|
||||
/// Whisper API Konfiguration
|
||||
const OPENAI_API_URL: &str = "https://api.openai.com/v1/audio/transcriptions";
|
||||
|
|
|
|||
153
src/lib/components/AnimatedCode.svelte
Normal file
153
src/lib/components/AnimatedCode.svelte
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
language?: string;
|
||||
wpm?: number;
|
||||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
let { code, language = 'text', wpm = 180, autoStart = true }: Props = $props();
|
||||
|
||||
let displayed = $state('');
|
||||
let playing = $state(false);
|
||||
let index = $state(0);
|
||||
let timer: number | null = null;
|
||||
|
||||
function charDelay(): number {
|
||||
// ~5 Zeichen pro Wort
|
||||
return 60000 / (wpm * 5);
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (playing) return;
|
||||
playing = true;
|
||||
step();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
playing = false;
|
||||
if (timer !== null) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
pause();
|
||||
displayed = '';
|
||||
index = 0;
|
||||
}
|
||||
|
||||
function step() {
|
||||
if (!playing || index >= code.length) {
|
||||
playing = false;
|
||||
return;
|
||||
}
|
||||
displayed += code[index];
|
||||
index++;
|
||||
timer = window.setTimeout(step, charDelay());
|
||||
}
|
||||
|
||||
function skipToEnd() {
|
||||
pause();
|
||||
displayed = code;
|
||||
index = code.length;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoStart) play();
|
||||
return () => {
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="animated-code">
|
||||
<div class="toolbar">
|
||||
<span class="lang">{language}</span>
|
||||
<div class="controls">
|
||||
{#if playing}
|
||||
<button onclick={pause} title="Pause">⏸</button>
|
||||
{:else}
|
||||
<button onclick={play} title="Abspielen">▶</button>
|
||||
{/if}
|
||||
<button onclick={reset} title="Zurücksetzen">↺</button>
|
||||
<button onclick={skipToEnd} title="Zum Ende">⏭</button>
|
||||
<span class="speed">{wpm} WPM</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="code-block"><code>{displayed}{#if playing}<span class="cursor">|</span>{/if}</code></pre>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.animated-code {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--bg-tertiary);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.lang {
|
||||
color: var(--accent);
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.speed {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.72rem;
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
margin: 0;
|
||||
padding: var(--spacing-md);
|
||||
font-family: 'JetBrains Mono', 'Cascadia Code', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
min-height: 60px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
animation: blink 1s step-end infinite;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
258
src/lib/components/HooksPanel.svelte
Normal file
258
src/lib/components/HooksPanel.svelte
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
interface HookConfig {
|
||||
name: string;
|
||||
event: string;
|
||||
enabled: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface HookExecution {
|
||||
timestamp: string;
|
||||
event: string;
|
||||
hook_name: string;
|
||||
duration_ms: number;
|
||||
success: boolean;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
let hooks = $state<HookConfig[]>([]);
|
||||
let executions = $state<HookExecution[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
async function loadHooks() {
|
||||
try {
|
||||
hooks = await invoke<HookConfig[]>('list_hooks');
|
||||
executions = await invoke<HookExecution[]>('get_hook_executions', { limit: 50 });
|
||||
} catch (err) {
|
||||
console.error('Hooks laden fehlgeschlagen:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(hook: HookConfig) {
|
||||
try {
|
||||
await invoke('set_hook_enabled', {
|
||||
event: hook.event,
|
||||
hookName: hook.name,
|
||||
enabled: !hook.enabled
|
||||
});
|
||||
await loadHooks();
|
||||
} catch (err) {
|
||||
console.error('Hook toggle fehlgeschlagen:', err);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadHooks();
|
||||
|
||||
// Live-Updates bei Hook-Ausführung
|
||||
const unlisten = listen<{ event: string; hooks: string[]; summary: string }>('hook-fired', () => {
|
||||
loadHooks();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then(fn => fn());
|
||||
};
|
||||
});
|
||||
|
||||
const eventLabels: Record<string, string> = {
|
||||
SessionStart: '🚀 Session-Start',
|
||||
PreToolUse: '🔧 Vor Tool-Aufruf',
|
||||
PostToolUse: '✅ Nach Tool-Aufruf',
|
||||
BeforeCompacting: '📦 Vor Compacting',
|
||||
AfterCompacting: '📦 Nach Compacting',
|
||||
ContextFailure: '❌ Context-Fehler',
|
||||
AgentStarted: '🤖 Agent-Start'
|
||||
};
|
||||
|
||||
function groupByEvent(items: HookConfig[]): Record<string, HookConfig[]> {
|
||||
const out: Record<string, HookConfig[]> = {};
|
||||
for (const h of items) {
|
||||
(out[h.event] ||= []).push(h);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hooks-panel">
|
||||
<h3>🪝 Hook-System</h3>
|
||||
<p class="hint">
|
||||
Hooks laufen automatisch bei bestimmten Events. Deaktiviere einzelne Hooks, wenn sie stören.
|
||||
</p>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Lade Hooks...</div>
|
||||
{:else}
|
||||
<div class="hook-groups">
|
||||
{#each Object.entries(groupByEvent(hooks)) as [event, hookList]}
|
||||
<div class="hook-group">
|
||||
<h4>{eventLabels[event] ?? event}</h4>
|
||||
{#each hookList as hook}
|
||||
<label class="hook-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hook.enabled}
|
||||
onchange={() => toggle(hook)}
|
||||
/>
|
||||
<div class="hook-info">
|
||||
<div class="hook-name">{hook.name}</div>
|
||||
<div class="hook-desc">{hook.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="executions">
|
||||
<h4>Letzte Ausführungen ({executions.length})</h4>
|
||||
{#if executions.length === 0}
|
||||
<div class="empty">Noch keine Hooks ausgeführt.</div>
|
||||
{:else}
|
||||
<ul class="execution-list">
|
||||
{#each executions.slice().reverse() as exec}
|
||||
<li class="execution" class:failure={!exec.success}>
|
||||
<span class="exec-time">{new Date(exec.timestamp).toLocaleTimeString()}</span>
|
||||
<span class="exec-event">{eventLabels[exec.event] ?? exec.event}</span>
|
||||
<span class="exec-name">{exec.hook_name}</span>
|
||||
<span class="exec-summary">{exec.summary}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hooks-panel {
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hook-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.hook-group {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.hook-group h4 {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.hook-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.hook-item:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.hook-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hook-name {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hook-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.executions {
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.executions h4 {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.execution-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.execution {
|
||||
display: grid;
|
||||
grid-template-columns: 70px 140px 160px 1fr;
|
||||
gap: var(--spacing-sm);
|
||||
padding: 2px var(--spacing-xs);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.execution.failure {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.exec-time {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.exec-event {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.exec-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.exec-summary {
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
222
src/lib/components/IdePanel.svelte
Normal file
222
src/lib/components/IdePanel.svelte
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface IdeStatus {
|
||||
connected: boolean;
|
||||
port: number;
|
||||
last_error: string | null;
|
||||
}
|
||||
|
||||
let status = $state<IdeStatus>({ connected: false, port: 7890, last_error: null });
|
||||
let activeFile = $state<string | null>(null);
|
||||
let cursorLine = $state<number | null>(null);
|
||||
let pollTimer: number | null = null;
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
status = await invoke<IdeStatus>('ide_connect', { port: status.port });
|
||||
if (status.connected) {
|
||||
await refreshState();
|
||||
startPolling();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('IDE connect:', err);
|
||||
status.last_error = String(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
try {
|
||||
await invoke('ide_disconnect');
|
||||
status = await invoke<IdeStatus>('ide_status');
|
||||
activeFile = null;
|
||||
cursorLine = null;
|
||||
stopPolling();
|
||||
} catch (err) {
|
||||
console.error('IDE disconnect:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshState() {
|
||||
if (!status.connected) return;
|
||||
try {
|
||||
const result = await invoke<{ openFile?: string; cursorLine?: number }>('ide_call', {
|
||||
command: 'getStatus',
|
||||
args: {}
|
||||
});
|
||||
activeFile = result.openFile ?? null;
|
||||
cursorLine = result.cursorLine ?? null;
|
||||
} catch (err) {
|
||||
console.error('IDE getStatus:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
pollTimer = window.setInterval(refreshState, 2000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer !== null) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function testPing() {
|
||||
try {
|
||||
const r = await invoke('ide_call', { command: 'ping', args: {} });
|
||||
alert(`Pong: ${JSON.stringify(r)}`);
|
||||
} catch (err) {
|
||||
alert(`Fehler: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
invoke<IdeStatus>('ide_status').then((s) => {
|
||||
status = s;
|
||||
if (status.connected) {
|
||||
startPolling();
|
||||
refreshState();
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
return stopPolling;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="ide-panel">
|
||||
<h3>🧩 VSCodium-Bridge</h3>
|
||||
|
||||
<div class="status" class:connected={status.connected}>
|
||||
{#if status.connected}
|
||||
✅ Verbunden auf Port {status.port}
|
||||
{:else}
|
||||
⚠️ Nicht verbunden
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if status.last_error}
|
||||
<div class="error">{status.last_error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="controls">
|
||||
<label>
|
||||
Port:
|
||||
<input type="number" bind:value={status.port} disabled={status.connected} />
|
||||
</label>
|
||||
{#if status.connected}
|
||||
<button onclick={disconnect}>Trennen</button>
|
||||
<button onclick={testPing}>Ping-Test</button>
|
||||
{:else}
|
||||
<button onclick={connect} class="primary">Verbinden</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if status.connected && activeFile}
|
||||
<div class="active-info">
|
||||
<div class="label">Aktive Datei:</div>
|
||||
<div class="value">{activeFile}</div>
|
||||
{#if cursorLine !== null}
|
||||
<div class="label">Zeile:</div>
|
||||
<div class="value">{cursorLine}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="hint">
|
||||
<strong>Setup:</strong> Extension unter <code>vscode-extension/</code> in VSCodium laden
|
||||
(F5 im Extension-Dev-Host, oder als .vsix paketieren). Extension startet automatisch
|
||||
einen WebSocket-Server auf Port 7890.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ide-panel {
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f87171;
|
||||
font-size: 0.8rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.controls input {
|
||||
width: 80px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.controls button.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.active-info {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: 0.82rem;
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.value {
|
||||
font-family: monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hint code {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
</style>
|
||||
70
src/lib/components/MermaidDiagram.svelte
Normal file
70
src/lib/components/MermaidDiagram.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let { code, id = `mermaid-${Math.random().toString(36).slice(2)}` }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function render() {
|
||||
if (!container) return;
|
||||
try {
|
||||
// @ts-expect-error - mermaid wird zur Laufzeit geladen (npm install mermaid erforderlich)
|
||||
const mermaidModule = await import('mermaid');
|
||||
const mermaid = mermaidModule.default;
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
|
||||
const { svg } = await mermaid.render(id, code);
|
||||
container.innerHTML = svg;
|
||||
error = null;
|
||||
} catch (err) {
|
||||
error = String(err);
|
||||
container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
code;
|
||||
render();
|
||||
});
|
||||
|
||||
onMount(render);
|
||||
</script>
|
||||
|
||||
<div class="mermaid-container">
|
||||
{#if error}
|
||||
<pre class="error">{error}</pre>
|
||||
{/if}
|
||||
<div bind:this={container} class="diagram"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mermaid-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.diagram {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.diagram :global(svg) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f87171;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
278
src/lib/components/ProgramsPanel.svelte
Normal file
278
src/lib/components/ProgramsPanel.svelte
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import IdePanel from './IdePanel.svelte';
|
||||
|
||||
interface XvfbStatus {
|
||||
running: boolean;
|
||||
display_num: number;
|
||||
pid: number | null;
|
||||
resolution: string;
|
||||
}
|
||||
|
||||
interface PlaywrightInfo {
|
||||
available: boolean;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
let section = $state<'ide' | 'playwright' | 'dbus' | 'xvfb'>('ide');
|
||||
|
||||
let xvfb = $state<XvfbStatus>({ running: false, display_num: 1, pid: null, resolution: '1920x1080x24' });
|
||||
let playwright = $state<PlaywrightInfo>({ available: false, hint: '' });
|
||||
let dbusServices = $state<string[]>([]);
|
||||
let dbusLoading = $state(false);
|
||||
let screenshot = $state<string | null>(null);
|
||||
|
||||
async function loadXvfb() {
|
||||
try {
|
||||
xvfb = await invoke<XvfbStatus>('xvfb_status');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function startXvfb() {
|
||||
try {
|
||||
xvfb = await invoke<XvfbStatus>('xvfb_start', {
|
||||
displayNum: xvfb.display_num,
|
||||
resolution: xvfb.resolution
|
||||
});
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopXvfb() {
|
||||
try {
|
||||
xvfb = await invoke<XvfbStatus>('xvfb_stop');
|
||||
screenshot = null;
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function takeScreenshot() {
|
||||
try {
|
||||
const b64 = await invoke<string>('xvfb_screenshot', { displayNum: xvfb.display_num });
|
||||
screenshot = `data:image/png;base64,${b64}`;
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlaywright() {
|
||||
try {
|
||||
playwright = await invoke<PlaywrightInfo>('playwright_info');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadDbus() {
|
||||
dbusLoading = true;
|
||||
try {
|
||||
dbusServices = await invoke<string[]>('dbus_list_services', { session: true });
|
||||
} catch (err) {
|
||||
dbusServices = [];
|
||||
alert(err);
|
||||
} finally {
|
||||
dbusLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadXvfb();
|
||||
loadPlaywright();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="programs-panel">
|
||||
<div class="section-tabs">
|
||||
<button class:active={section === 'ide'} onclick={() => (section = 'ide')}>🧩 VSCodium</button>
|
||||
<button class:active={section === 'playwright'} onclick={() => (section = 'playwright')}>🎭 Playwright</button>
|
||||
<button class:active={section === 'dbus'} onclick={() => (section = 'dbus')}>🔌 D-Bus</button>
|
||||
<button class:active={section === 'xvfb'} onclick={() => (section = 'xvfb')}>🖥️ Xvfb</button>
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
{#if section === 'ide'}
|
||||
<IdePanel />
|
||||
{:else if section === 'playwright'}
|
||||
<h3>🎭 Playwright (Browser-Automation)</h3>
|
||||
<div class="info-card">
|
||||
<strong>Status:</strong>
|
||||
{playwright.available ? '✅ MCP-Server konfiguriert' : '⚠️ Nicht verfügbar'}
|
||||
</div>
|
||||
<div class="hint">{playwright.hint}</div>
|
||||
{:else if section === 'dbus'}
|
||||
<h3>🔌 D-Bus Services</h3>
|
||||
<div class="controls">
|
||||
<button onclick={loadDbus} disabled={dbusLoading}>
|
||||
{dbusLoading ? 'Lade...' : 'Services laden'}
|
||||
</button>
|
||||
</div>
|
||||
{#if dbusServices.length > 0}
|
||||
<ul class="dbus-list">
|
||||
{#each dbusServices as svc}
|
||||
<li>{svc}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="empty">Noch keine Services geladen.</div>
|
||||
{/if}
|
||||
<div class="hint">
|
||||
Aufruf via Chat: Claude kann <code>dbus_call(service, path, method)</code> nutzen.
|
||||
</div>
|
||||
{:else if section === 'xvfb'}
|
||||
<h3>🖥️ Virtuelles Display (Xvfb)</h3>
|
||||
<div class="info-card" class:running={xvfb.running}>
|
||||
{xvfb.running ? `✅ Läuft auf :${xvfb.display_num} (PID ${xvfb.pid})` : '⚠️ Nicht aktiv'}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label>
|
||||
Display:
|
||||
<input type="number" bind:value={xvfb.display_num} disabled={xvfb.running} />
|
||||
</label>
|
||||
<label>
|
||||
Auflösung:
|
||||
<input bind:value={xvfb.resolution} disabled={xvfb.running} />
|
||||
</label>
|
||||
{#if xvfb.running}
|
||||
<button onclick={stopXvfb}>Stopp</button>
|
||||
<button onclick={takeScreenshot}>📸 Screenshot</button>
|
||||
{:else}
|
||||
<button onclick={startXvfb} class="primary">Start</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if screenshot}
|
||||
<img src={screenshot} alt="Xvfb Screenshot" class="screenshot" />
|
||||
{/if}
|
||||
<div class="hint">
|
||||
Starte Programme auf diesem Display via
|
||||
<code>DISPLAY=:1 firefox</code>. Claude kann Screenshots aufnehmen.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.programs-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: var(--spacing-xs);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.section-tabs button {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.78rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.section-tabs button.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.info-card.running {
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
align-items: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.controls input {
|
||||
width: 140px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.controls button.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.dbus-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dbus-list li {
|
||||
padding: 2px var(--spacing-xs);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.screenshot {
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hint code {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -141,6 +141,13 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="titlebar-right">
|
||||
<button
|
||||
class="teach-btn"
|
||||
title="Schulungsmodus (Präsentations-Fenster)"
|
||||
onclick={() => invoke('presentation_open').catch(e => console.error(e))}
|
||||
>
|
||||
🎓
|
||||
</button>
|
||||
{#if $currentModel}
|
||||
<span class="model-badge">{$currentModel.replace('claude-', '').replace(/-\d{8}$/, '').replace(/(\D)-(\d)/g, '$1 $2').replace(/(\d)-(\d)/g, '$1.$2').replace(/\b[a-z]/g, c => c.toUpperCase())}</span>
|
||||
{/if}
|
||||
|
|
@ -315,4 +322,19 @@
|
|||
color: #06b6d4;
|
||||
background: rgba(6, 182, 212, 0.12);
|
||||
}
|
||||
|
||||
.teach-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.teach-btn:hover {
|
||||
background: rgba(96, 165, 250, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
||||
import MonitorPanel from '$lib/components/MonitorPanel.svelte';
|
||||
import PerformancePanel from '$lib/components/PerformancePanel.svelte';
|
||||
import HooksPanel from '$lib/components/HooksPanel.svelte';
|
||||
import ProgramsPanel from '$lib/components/ProgramsPanel.svelte';
|
||||
|
||||
let activeMiddleTab = 'activity';
|
||||
let activeRightTab = 'agents';
|
||||
|
|
@ -23,11 +25,13 @@
|
|||
{ id: 'knowledge', label: 'Wissen', icon: '📚' },
|
||||
{ id: 'memory', label: 'Gedächtnis', icon: '🧠' },
|
||||
{ id: 'audit', label: 'Historie', icon: '📝' },
|
||||
{ id: 'programs', label: 'Programme', icon: '🖥️' },
|
||||
];
|
||||
|
||||
const rightTabs = [
|
||||
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
||||
{ id: 'context', label: 'Context', icon: '📌' },
|
||||
{ id: 'hooks', label: 'Hooks', icon: '🪝' },
|
||||
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
||||
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
||||
];
|
||||
|
|
@ -78,6 +82,8 @@
|
|||
<MemoryPanel />
|
||||
{:else if activeMiddleTab === 'audit'}
|
||||
<AuditLog />
|
||||
{:else if activeMiddleTab === 'programs'}
|
||||
<ProgramsPanel />
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
|
|
@ -104,6 +110,8 @@
|
|||
<AgentView />
|
||||
{:else if activeRightTab === 'context'}
|
||||
<ContextPanel />
|
||||
{:else if activeRightTab === 'hooks'}
|
||||
<HooksPanel />
|
||||
{:else if activeRightTab === 'guards'}
|
||||
<GuardRailsPanel />
|
||||
{:else if activeRightTab === 'settings'}
|
||||
|
|
|
|||
179
src/routes/presentation/+page.svelte
Normal file
179
src/routes/presentation/+page.svelte
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import MermaidDiagram from '$lib/components/MermaidDiagram.svelte';
|
||||
import AnimatedCode from '$lib/components/AnimatedCode.svelte';
|
||||
|
||||
interface Slide {
|
||||
type: 'mermaid' | 'code' | 'text';
|
||||
content: string;
|
||||
language?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
let slides = $state<Slide[]>([
|
||||
{
|
||||
type: 'text',
|
||||
title: 'Willkommen',
|
||||
content: '🎓 Schulungsmodus bereit.\n\nClaude schickt dir hier Mindmaps, Flowcharts und animierten Code.'
|
||||
}
|
||||
]);
|
||||
let currentIndex = $state(0);
|
||||
let wpm = $state(180);
|
||||
let paused = $state(false);
|
||||
|
||||
const current = $derived(slides[currentIndex]);
|
||||
|
||||
function next() {
|
||||
if (currentIndex < slides.length - 1) currentIndex++;
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (currentIndex > 0) currentIndex--;
|
||||
}
|
||||
|
||||
function addSlide(slide: Slide) {
|
||||
slides = [...slides, slide];
|
||||
currentIndex = slides.length - 1;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const unlistenSlide = listen<Slide>('presentation-slide', (event) => {
|
||||
addSlide(event.payload);
|
||||
});
|
||||
const unlistenClear = listen('presentation-clear', () => {
|
||||
slides = [];
|
||||
currentIndex = 0;
|
||||
});
|
||||
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') next();
|
||||
else if (e.key === 'ArrowLeft') prev();
|
||||
else if (e.key === 'p') paused = !paused;
|
||||
};
|
||||
window.addEventListener('keydown', keyHandler);
|
||||
|
||||
return () => {
|
||||
unlistenSlide.then(fn => fn());
|
||||
unlistenClear.then(fn => fn());
|
||||
window.removeEventListener('keydown', keyHandler);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="presentation">
|
||||
<div class="content">
|
||||
{#if current}
|
||||
{#if current.title}
|
||||
<h1>{current.title}</h1>
|
||||
{/if}
|
||||
|
||||
{#if current.type === 'mermaid'}
|
||||
<MermaidDiagram code={current.content} />
|
||||
{:else if current.type === 'code'}
|
||||
<AnimatedCode
|
||||
code={current.content}
|
||||
language={current.language ?? 'text'}
|
||||
{wpm}
|
||||
autoStart={!paused}
|
||||
/>
|
||||
{:else}
|
||||
<pre class="text-slide">{current.content}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer class="controls">
|
||||
<button onclick={prev} disabled={currentIndex === 0}>◀◀</button>
|
||||
<button onclick={() => paused = !paused}>{paused ? '▶' : '⏸'}</button>
|
||||
<button onclick={next} disabled={currentIndex >= slides.length - 1}>▶▶</button>
|
||||
<label>
|
||||
Tempo:
|
||||
<input type="range" min="60" max="400" bind:value={wpm} />
|
||||
<span>{wpm} WPM</span>
|
||||
</label>
|
||||
<span class="counter">{currentIndex + 1} / {slides.length}</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(html, body) {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.presentation {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin: 0 0 1rem 0;
|
||||
text-align: center;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.text-slide {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.6;
|
||||
max-width: 70ch;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 0.8rem;
|
||||
background: #1e293b;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 0.4rem 1rem;
|
||||
background: #334155;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.controls button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.controls button:hover:not(:disabled) {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
37
vscode-extension/README.md
Normal file
37
vscode-extension/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Claude Desktop Bridge — VSCode Extension
|
||||
|
||||
Ermöglicht Claude Desktop, VSCodium/VSCode zu steuern — ohne Maus-Simulation.
|
||||
|
||||
## Installation (Entwicklung)
|
||||
|
||||
```bash
|
||||
cd vscode-extension
|
||||
npm install
|
||||
npm run compile
|
||||
```
|
||||
|
||||
Dann in VSCodium:
|
||||
- `F5` für Extension Development Host, oder
|
||||
- `vsce package` → `.vsix` → Installieren
|
||||
|
||||
## Features
|
||||
|
||||
- WebSocket-Server auf Port 7890 (konfigurierbar)
|
||||
- Commands: openFile, goToLine, formatDocument, findInFiles, openTerminal, getStatus, executeCommand
|
||||
- Status-Bar-Anzeige des Verbindungsstatus
|
||||
|
||||
## Protokoll
|
||||
|
||||
Request:
|
||||
```json
|
||||
{ "id": "uuid", "command": "openFile", "args": { "path": "/path/to/file" } }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "uuid", "result": { "opened": "/path/to/file" } }
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
Server lauscht nur auf `127.0.0.1` — kein Zugriff von außen.
|
||||
54
vscode-extension/package.json
Normal file
54
vscode-extension/package.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "claude-desktop-bridge",
|
||||
"displayName": "Claude Desktop Bridge",
|
||||
"description": "Steuert VSCodium von Claude Desktop aus",
|
||||
"version": "0.1.0",
|
||||
"publisher": "data-it",
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"categories": ["Other"],
|
||||
"activationEvents": ["onStartupFinished"],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "claude-desktop.connect",
|
||||
"title": "Claude Desktop: Verbindung starten"
|
||||
},
|
||||
{
|
||||
"command": "claude-desktop.disconnect",
|
||||
"title": "Claude Desktop: Verbindung beenden"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Claude Desktop",
|
||||
"properties": {
|
||||
"claudeDesktop.port": {
|
||||
"type": "number",
|
||||
"default": 7890,
|
||||
"description": "WebSocket-Port fuer Claude Desktop Verbindung"
|
||||
},
|
||||
"claudeDesktop.autoConnect": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Automatisch beim Start verbinden"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"@types/ws": "^8.5.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
185
vscode-extension/src/extension.ts
Normal file
185
vscode-extension/src/extension.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// Claude Desktop Bridge - VSCode Extension
|
||||
// WebSocket-Server, der Commands von Claude Desktop empfaengt
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
|
||||
interface BridgeRequest {
|
||||
id: string;
|
||||
command: string;
|
||||
args?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface BridgeResponse {
|
||||
id: string;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
let wss: WebSocketServer | null = null;
|
||||
let statusBar: vscode.StatusBarItem;
|
||||
let connectedClients = 0;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('[Claude Desktop Bridge] aktiviert');
|
||||
|
||||
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||
statusBar.text = '$(broadcast) Claude: aus';
|
||||
statusBar.command = 'claude-desktop.connect';
|
||||
statusBar.show();
|
||||
context.subscriptions.push(statusBar);
|
||||
|
||||
const connectCmd = vscode.commands.registerCommand('claude-desktop.connect', () => {
|
||||
startServer(context);
|
||||
});
|
||||
const disconnectCmd = vscode.commands.registerCommand('claude-desktop.disconnect', () => {
|
||||
stopServer();
|
||||
});
|
||||
context.subscriptions.push(connectCmd, disconnectCmd);
|
||||
|
||||
const cfg = vscode.workspace.getConfiguration('claudeDesktop');
|
||||
if (cfg.get<boolean>('autoConnect')) {
|
||||
startServer(context);
|
||||
}
|
||||
}
|
||||
|
||||
function startServer(context: vscode.ExtensionContext) {
|
||||
if (wss) {
|
||||
vscode.window.showInformationMessage('Claude Bridge laeuft bereits.');
|
||||
return;
|
||||
}
|
||||
|
||||
const port = vscode.workspace.getConfiguration('claudeDesktop').get<number>('port', 7890);
|
||||
|
||||
try {
|
||||
wss = new WebSocketServer({ port, host: '127.0.0.1' });
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(`Claude Bridge Port ${port} blockiert: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
wss.on('connection', (ws: WebSocket) => {
|
||||
connectedClients++;
|
||||
updateStatus();
|
||||
console.log('[Claude Desktop Bridge] Client verbunden');
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
let req: BridgeRequest;
|
||||
try {
|
||||
req = JSON.parse(data.toString());
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const response: BridgeResponse = { id: req.id };
|
||||
try {
|
||||
response.result = await handleCommand(req.command, req.args ?? {});
|
||||
} catch (err: any) {
|
||||
response.error = err?.message ?? String(err);
|
||||
}
|
||||
ws.send(JSON.stringify(response));
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
connectedClients--;
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('error', (err) => {
|
||||
vscode.window.showErrorMessage(`Claude Bridge Fehler: ${err.message}`);
|
||||
});
|
||||
|
||||
updateStatus();
|
||||
vscode.window.showInformationMessage(`Claude Bridge auf Port ${port} gestartet`);
|
||||
}
|
||||
|
||||
function stopServer() {
|
||||
if (!wss) return;
|
||||
wss.close();
|
||||
wss = null;
|
||||
connectedClients = 0;
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
function updateStatus() {
|
||||
if (!wss) {
|
||||
statusBar.text = '$(broadcast) Claude: aus';
|
||||
statusBar.backgroundColor = undefined;
|
||||
} else if (connectedClients > 0) {
|
||||
statusBar.text = `$(check) Claude: verbunden (${connectedClients})`;
|
||||
statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
||||
} else {
|
||||
statusBar.text = '$(broadcast) Claude: wartet';
|
||||
statusBar.backgroundColor = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Command-Handler fuer Claude Desktop
|
||||
async function handleCommand(command: string, args: Record<string, unknown>): Promise<unknown> {
|
||||
switch (command) {
|
||||
case 'ping':
|
||||
return { pong: true, version: vscode.version };
|
||||
|
||||
case 'openFile': {
|
||||
const path = args.path as string;
|
||||
const uri = vscode.Uri.file(path);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
return { opened: path };
|
||||
}
|
||||
|
||||
case 'goToLine': {
|
||||
const line = args.line as number;
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) throw new Error('Kein aktiver Editor');
|
||||
const pos = new vscode.Position(Math.max(0, line - 1), 0);
|
||||
editor.selection = new vscode.Selection(pos, pos);
|
||||
editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter);
|
||||
return { line };
|
||||
}
|
||||
|
||||
case 'formatDocument': {
|
||||
await vscode.commands.executeCommand('editor.action.formatDocument');
|
||||
return { formatted: true };
|
||||
}
|
||||
|
||||
case 'findInFiles': {
|
||||
const query = args.query as string;
|
||||
await vscode.commands.executeCommand('workbench.action.findInFiles', { query });
|
||||
return { query };
|
||||
}
|
||||
|
||||
case 'openTerminal': {
|
||||
const terminal = vscode.window.createTerminal(args.name as string ?? 'Claude');
|
||||
terminal.show();
|
||||
if (args.command) {
|
||||
terminal.sendText(args.command as string);
|
||||
}
|
||||
return { created: true };
|
||||
}
|
||||
|
||||
case 'getStatus': {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
return {
|
||||
openFile: editor?.document.fileName,
|
||||
cursorLine: editor ? editor.selection.active.line + 1 : null,
|
||||
language: editor?.document.languageId,
|
||||
workspaceFolders: vscode.workspace.workspaceFolders?.map(f => f.uri.fsPath),
|
||||
};
|
||||
}
|
||||
|
||||
case 'executeCommand': {
|
||||
const cmd = args.command as string;
|
||||
const cmdArgs = (args.args as unknown[]) ?? [];
|
||||
return await vscode.commands.executeCommand(cmd, ...cmdArgs);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unbekannter Command: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
stopServer();
|
||||
}
|
||||
13
vscode-extension/tsconfig.json
Normal file
13
vscode-extension/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021"],
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
Loading…
Reference in a new issue