Security-Fixes + UI-Verbesserungen: Stop-Button, Textfeld, Agent-Filter
All checks were successful
Build AppImage / build (push) Has been skipped
All checks were successful
Build AppImage / build (push) Has been skipped
Backend: - Credentials aus Code entfernt → ENV-Variablen mit Fallback - File-Traversal in Update-Download verhindert (Path-Sanitization) - CLI-Injection bei D-Bus mit Whitelist-Validierung abgesichert Frontend: - Stop-Button dezenter (kleinere Schrift, gedämpftes Rot, kein Pulsieren) - Stop löscht keine Session/Messages mehr — nur Agents stoppen - Textfeld nicht mehr blockiert während Claude arbeitet (Einwände möglich) - Agent-Filter "Nur aktive" wird in localStorage persistent gespeichert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
427fa858a9
commit
3993387977
8 changed files with 192 additions and 44 deletions
139
README.md
139
README.md
|
|
@ -2,6 +2,145 @@
|
|||
|
||||
Eigenständige Desktop-Anwendung die Claude Code als Backend nutzt, mit nativem UI, Live-Übersicht und kontrolliertem OS-Zugriff.
|
||||
|
||||
## Status (Stand 2026-04-20)
|
||||
|
||||
**Variante A (Native Desktop-App) ist umgesetzt** — Tauri 2.0 + SvelteKit 5, Phase 1-13 fertig (siehe [ROADMAP.md](ROADMAP.md)). Variante B (autonome VM) bleibt Vision.
|
||||
|
||||
Was läuft:
|
||||
- 4-Panel-Layout, 24 UI-Komponenten (Chat, Activity, Memory, Audit, Knowledge, Voice, Hooks, IDE, Programs, Performance, Settings, …)
|
||||
- 16 Rust-Backend-Module (`claude.rs`, `db.rs`, `guard.rs`, `memory.rs`, `voice.rs`, `hooks.rs`, `ide.rs`, …)
|
||||
- Sprach-Interface mit Push-to-Talk (`VoicePanel`), Whisper STT
|
||||
- Modell-Auswahl Haiku/Sonnet/Opus, Token-/Kosten-Anzeige
|
||||
- Subagent-Hierarchie, Multi-Agent-Modi, Hook-System
|
||||
- Session-Persistenz (SQLite), Audit-Log
|
||||
- VS-Code-Extension `claude-desktop-bridge` (steuert VSCodium aus der App heraus, WebSocket-Port 7890)
|
||||
- CI/CD-Pipeline (Forgejo Actions) → AppImage in Package-Registry
|
||||
|
||||
## Installation
|
||||
|
||||
### AppImage (Debian/Ubuntu/Fedora/Arch — **nicht NixOS**)
|
||||
|
||||
```bash
|
||||
mkdir -p ~/Applications
|
||||
curl -sSL -o ~/Applications/Claude-Desktop.AppImage \
|
||||
-u "token:<dein-forgejo-token>" \
|
||||
'https://git.data-it-solution.de/api/packages/data/generic/claude-desktop/latest/Claude-Desktop_0.1.0_amd64.AppImage'
|
||||
chmod +x ~/Applications/Claude-Desktop.AppImage
|
||||
~/Applications/Claude-Desktop.AppImage
|
||||
```
|
||||
|
||||
Auf NixOS hat das AppImage einen WebKit2GTK ↔ Mesa ABI-Konflikt → siehe nächster Abschnitt.
|
||||
|
||||
### NixOS — nativer Build (Pflicht)
|
||||
|
||||
Das AppImage läuft auf NixOS nicht (KB-Eintrag #381 zur Diagnose). Stattdessen lokal bauen mit der mitgelieferten `shell.nix`:
|
||||
|
||||
```bash
|
||||
cd "/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop"
|
||||
|
||||
# Cargo-Target auf lokales tmpfs (SMB-Mount macht Cargo-Build-Errors)
|
||||
CARGO_TARGET_DIR=/tmp/claude-target \
|
||||
nix-shell shell.nix --run 'npm ci && npm run tauri build -- --bundles appimage'
|
||||
|
||||
# Tauri-Bundling kann an pkg-config-Bug scheitern — Binary reicht aber:
|
||||
nix-shell shell.nix --run /tmp/claude-target/release/claude-desktop
|
||||
```
|
||||
|
||||
Permanenter Wrapper:
|
||||
|
||||
```bash
|
||||
cat > ~/.local/bin/claude-desktop <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cd "/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop"
|
||||
exec nix-shell shell.nix --run /tmp/claude-target/release/claude-desktop
|
||||
EOF
|
||||
chmod +x ~/.local/bin/claude-desktop
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
cd "/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop"
|
||||
nix-shell shell.nix --run 'npm ci && npm run tauri:dev' # mit Hot-Reload
|
||||
```
|
||||
|
||||
## CI/CD-Pipeline
|
||||
|
||||
Workflow: `.forgejo/workflows/build-appimage.yml`
|
||||
|
||||
| Trigger | Was passiert |
|
||||
|---|---|
|
||||
| Push auf `main` mit `[appimage]` in Commit-Message | AppImage bauen + in Package-Registry hochladen |
|
||||
| Push eines Tags `v*` | zusätzlich als Release-Asset anhängen |
|
||||
|
||||
**Runner:** `16-Forgejo-Runner-AppImage` (Debian Bookworm, glibc, mit linuxdeploy + appimagetool + Tauri-Build-Stack vorinstalliert). Der Standard-Alpine-Runner kann Tauri-AppImages nicht bauen (musl-Inkompatibilität von linuxdeploy). Setup des Runners: KB-Eintrag #371.
|
||||
|
||||
**Workflow-Eigenheiten** (gut zu wissen wenn was ändern):
|
||||
- AppImage-Filename mit Leerzeichen (`Claude Desktop_…`) wird vor Upload zu `Claude-Desktop_…` umbenannt (curl-URL-Bug)
|
||||
- Vor jedem Upload werden alte Versionen gelöscht (Forgejo Package-Registry weist PUT auf existierenden Pfad mit HTTP 409 ab)
|
||||
- Custom-AppRun mit NixOS-Detection wird nach `tauri build` eingesetzt + mit `appimagetool` re-bundled. Der Hook `apprun-hooks/linuxdeploy-plugin-gtk.sh` darf dabei nicht überschrieben werden (KB #384), sonst finden WebKit-Subprozesse ihre Helpers nicht
|
||||
- Ntfy-Notifications (Build-Start/Success/Failure) — Topic `vk-builds`
|
||||
|
||||
Ntfy-Setup für andere Projekte: KB #190/#191/#220.
|
||||
|
||||
## VS-Code-Extension `claude-desktop-bridge`
|
||||
|
||||
Eigenes Subprojekt unter [vscode-extension/](vscode-extension/). Ermöglicht Claude Desktop, VSCodium fernzusteuern (Datei öffnen, Cursor-Position setzen, Terminal-Befehle absetzen) über einen WebSocket-Server (Port 7890 default).
|
||||
|
||||
```bash
|
||||
cd vscode-extension
|
||||
npm ci && npm run compile
|
||||
# Verpacktes vsix: claude-desktop-bridge-0.1.0.vsix
|
||||
codium --install-extension claude-desktop-bridge-0.1.0.vsix
|
||||
```
|
||||
|
||||
Befehle in VSCodium: `Claude Desktop: Verbindung starten/beenden`. Die App meldet sich beim Start automatisch.
|
||||
|
||||
## Projektstruktur (Stand)
|
||||
|
||||
```
|
||||
ClaudeDesktop/
|
||||
├── src-tauri/src/ # 16 Rust-Module
|
||||
│ ├── main.rs # Entry (setzt WEBKIT_DISABLE_*-Defaults für Linux)
|
||||
│ ├── lib.rs # Tauri-App-Setup
|
||||
│ ├── claude.rs # Claude Agent SDK Integration
|
||||
│ ├── db.rs # MySQL (claude-DB) + SQLite-Persistierung
|
||||
│ ├── guard.rs # Guard-Rails (Critical/Moderate/Safe)
|
||||
│ ├── hooks.rs # Hook-System
|
||||
│ ├── memory.rs # Memory/Knowledge-Graph
|
||||
│ ├── voice.rs # Whisper STT + Voice-Activity-Detection
|
||||
│ ├── ide.rs # Bridge zur VSCodium-Extension
|
||||
│ ├── audit.rs, knowledge.rs, programs.rs, session.rs,
|
||||
│ │ teaching.rs, update.rs, context.rs
|
||||
│ └── ...
|
||||
├── src/ # SvelteKit Frontend
|
||||
│ ├── routes/+layout.svelte
|
||||
│ ├── routes/+page.svelte
|
||||
│ ├── routes/presentation/+page.svelte
|
||||
│ └── lib/components/ # 24 UI-Panels
|
||||
├── vscode-extension/ # VSCodium-Bridge
|
||||
├── .forgejo/workflows/ # CI/CD
|
||||
├── shell.nix # NixOS Dev-Shell
|
||||
├── ROADMAP.md # Phase-Status
|
||||
├── TEST-ROADMAP.md # Test-Plan
|
||||
├── tools.yaml # MCP-Tool-Inventar
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Wissensbasis-Referenzen (für künftige Sessions)
|
||||
|
||||
| KB-ID | Thema |
|
||||
|---|---|
|
||||
| #311 | Diese Pipeline (Workflow-Tricks) |
|
||||
| #371 | Debian Forgejo-Runner Setup |
|
||||
| #372 | libssl-dev für openssl-sys |
|
||||
| #381 | NixOS WebKit2GTK EGL-Crash → native build |
|
||||
| #382 | Cargo auf SMB → CARGO_TARGET_DIR umleiten |
|
||||
| #384 | Custom-AppRun + linuxdeploy-Hook |
|
||||
| #248 | Tauri 2.0 + SvelteKit auf NixOS shell.nix |
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Claude Code in VSCodium funktioniert, hat aber Grenzen:
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ use mysql_async::{Pool, prelude::*};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
/// Verbindungskonfiguration
|
||||
const MYSQL_HOST: &str = "192.168.155.11";
|
||||
/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults
|
||||
const MYSQL_PORT: u16 = 3306;
|
||||
const MYSQL_USER: &str = "claude";
|
||||
const MYSQL_PASS: &str = "8715";
|
||||
const MYSQL_DB: &str = "claude";
|
||||
|
||||
fn mysql_host() -> String { std::env::var("CLAUDE_MYSQL_HOST").unwrap_or_else(|_| "192.168.155.11".to_string()) }
|
||||
fn mysql_user() -> String { std::env::var("CLAUDE_MYSQL_USER").unwrap_or_else(|_| "claude".to_string()) }
|
||||
fn mysql_pass() -> String { std::env::var("CLAUDE_MYSQL_PASS").unwrap_or_else(|_| "8715".to_string()) }
|
||||
fn mysql_db() -> String { std::env::var("CLAUDE_MYSQL_DB").unwrap_or_else(|_| "claude".to_string()) }
|
||||
|
||||
/// Wissenseintrag aus der knowledge-Tabelle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -59,7 +60,7 @@ pub struct CategoryInfo {
|
|||
fn create_pool() -> Pool {
|
||||
let url = format!(
|
||||
"mysql://{}:{}@{}:{}/{}",
|
||||
MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB
|
||||
mysql_user(), mysql_pass(), mysql_host(), MYSQL_PORT, mysql_db()
|
||||
);
|
||||
Pool::new(url.as_str())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,17 @@ pub async fn dbus_call(
|
|||
) -> Result<DbusCallResult, String> {
|
||||
let bus = if session.unwrap_or(true) { "--session" } else { "--system" };
|
||||
|
||||
// Whitelist-Validierung gegen CLI-Injection
|
||||
if !service.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-') {
|
||||
return Err("Ungültiger Service-Name".to_string());
|
||||
}
|
||||
if !path.chars().all(|c| c.is_alphanumeric() || c == '/' || c == '.' || c == '_' || c == '-') {
|
||||
return Err("Ungültiger D-Bus Pfad".to_string());
|
||||
}
|
||||
if !method.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-') {
|
||||
return Err("Ungültiger Methodenname".to_string());
|
||||
}
|
||||
|
||||
let mut cmd = Command::new("dbus-send");
|
||||
cmd.arg("--print-reply")
|
||||
.arg(bus)
|
||||
|
|
|
|||
|
|
@ -142,6 +142,11 @@ pub async fn download_update(
|
|||
std::fs::create_dir_all(&cache_dir).ok();
|
||||
|
||||
let file_name = download_url.split('/').last().unwrap_or("update.AppImage");
|
||||
// Sanitize: Nur Basename, keine Pfad-Traversal (z.B. "../böse.sh")
|
||||
let file_name = std::path::Path::new(file_name)
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.unwrap_or("update.AppImage");
|
||||
let download_path = cache_dir.join(file_name);
|
||||
|
||||
let mut file = std::fs::File::create(&download_path)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
import type { Agent } from '$lib/stores/app';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
let onlyActive = $state(false);
|
||||
// Filter-State persistent in localStorage speichern
|
||||
let onlyActive = $state(localStorage.getItem('agentFilter_onlyActive') === 'true');
|
||||
$effect(() => {
|
||||
localStorage.setItem('agentFilter_onlyActive', String(onlyActive));
|
||||
});
|
||||
|
||||
// Gefilterter Tree: wenn onlyActive, dann nur Agents mit status==='active'
|
||||
const filteredTree = derived([agentTree], ([$tree]) => {
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@
|
|||
|
||||
async function sendMessage() {
|
||||
const text = $currentInput.trim();
|
||||
if (!text || $isProcessing) return;
|
||||
if (!text) return;
|
||||
|
||||
// Auto-Session erstellen falls keine aktiv
|
||||
let sessionId = get(currentSessionId);
|
||||
|
|
@ -747,7 +747,7 @@
|
|||
bind:value={$currentInput}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)"
|
||||
disabled={$isProcessing || isRecording}
|
||||
disabled={isRecording}
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="input-buttons">
|
||||
|
|
@ -768,7 +768,7 @@
|
|||
<button
|
||||
class="send-button"
|
||||
onclick={sendMessage}
|
||||
disabled={!$currentInput.trim() || $isProcessing || isRecording}
|
||||
disabled={!$currentInput.trim() || isRecording}
|
||||
>
|
||||
{#if $isProcessing}
|
||||
⏳
|
||||
|
|
|
|||
|
|
@ -30,30 +30,27 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: linear-gradient(135deg, var(--error), #c0392b);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid var(--error);
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: #c53030;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid rgba(197, 48, 48, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 15px rgba(218, 68, 83, 0.3);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.stop-button:not(.disabled):hover {
|
||||
background: linear-gradient(135deg, #e74c3c, var(--error));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(218, 68, 83, 0.4);
|
||||
background: #b52828;
|
||||
color: white;
|
||||
border-color: rgba(197, 48, 48, 0.8);
|
||||
}
|
||||
|
||||
.stop-button:not(.disabled):active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(218, 68, 83, 0.3);
|
||||
background: #a02020;
|
||||
}
|
||||
|
||||
.stop-button.disabled {
|
||||
|
|
@ -61,26 +58,15 @@
|
|||
border-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.stop-icon {
|
||||
font-size: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.stop-hint {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Animierter Rand wenn aktiv */
|
||||
.stop-button:not(.disabled) {
|
||||
animation: border-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes border-pulse {
|
||||
0%, 100% { border-color: var(--error); }
|
||||
50% { border-color: #e88; }
|
||||
opacity: 0.65;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
updateAgentStatus,
|
||||
addToolCall,
|
||||
completeToolCall,
|
||||
clearAll,
|
||||
currentModel,
|
||||
sessionStats,
|
||||
contextUsage,
|
||||
|
|
@ -333,12 +332,15 @@ export async function initEventListeners(): Promise<void> {
|
|||
})
|
||||
);
|
||||
|
||||
// STOPP-Signal
|
||||
// STOPP-Signal — nur Agents stoppen, Messages/Session bleiben erhalten
|
||||
listeners.push(
|
||||
await listen('agents-stopped', () => {
|
||||
console.log('🛑 STOPP-Signal empfangen');
|
||||
streamingMessageId = null;
|
||||
clearAll();
|
||||
// Alle Agents auf "stopped" setzen, aber Messages NICHT löschen
|
||||
agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const })));
|
||||
toolCalls.set([]);
|
||||
isProcessing.set(false);
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue