feat: Quick-Actions (Ctrl+K) + GStreamer-Fix für Produktion [appimage]
Some checks failed
Build AppImage / build (push) Failing after 14s
Some checks failed
Build AppImage / build (push) Failing after 14s
Quick-Actions Palette mit VS-Code-artigem UI: Suche, Kategorien (Build/Git/Session/Navigation/Voice/Tools), Keyboard-Navigation. Nix-Wrapper enthält jetzt GStreamer + PipeWire für Mikrofon-Support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e87ac9ffc1
commit
9c6026de40
7 changed files with 640 additions and 22 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -6,9 +6,15 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Unreleased] - 2025-04-20
|
## [Unreleased] - 2026-04-21
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
- **Quick-Actions Palette (Ctrl+K)**: VS-Code-artige Kommandopalette mit Suche, Kategorien (Build, Git, Session, Navigation, Voice, Tools), Keyboard-Navigation (`QuickActions.svelte`)
|
||||||
|
- **Lokales Voice (Phase 2.2)**: whisper-cli STT + piper-tts TTS, komplett lokal ohne OpenAI-API (`voice.rs`, `VoicePanel.svelte`)
|
||||||
|
- **Chat-Detach**: Chat in separates Fenster herauslösen, Platz für andere Panels, Zurückholen per Button (`chat_window.rs`, `+page.svelte`)
|
||||||
|
- **Aktivitäts-Phasen**: 4 Zustände (Denkt nach/Streamt/Tool-Nutzung/Subagent) statt nur "Denkt nach..." (`events.ts`, `ChatPanel.svelte`)
|
||||||
|
- **Settings-Panel (VS Code Stil)**: Suchfeld, Kategorien-Sidebar, Commands/Hooks/Permissions-Verwaltung (`SettingsPanel.svelte`)
|
||||||
|
- **GStreamer im Nix-Wrapper**: Mikrofon funktioniert jetzt auch in Produktion (PipeWire + gst-plugins) (`nix/default.nix`)
|
||||||
- **Slash-Command Autocomplete**: `/`-Eingabe im Chat öffnet Dropdown mit allen Commands, Skills und Built-ins (`commands.rs`, `CommandPalette.svelte`)
|
- **Slash-Command Autocomplete**: `/`-Eingabe im Chat öffnet Dropdown mit allen Commands, Skills und Built-ins (`commands.rs`, `CommandPalette.svelte`)
|
||||||
- **KB-Hints Injection**: Jede Nachricht an Claude bekommt automatisch relevante Wissensbasis-Einträge (`claude.rs`, `knowledge.rs`)
|
- **KB-Hints Injection**: Jede Nachricht an Claude bekommt automatisch relevante Wissensbasis-Einträge (`claude.rs`, `knowledge.rs`)
|
||||||
- **Voice-zu-Claude-Pipeline**: Spracheingabe wird transkribiert, an Claude gesendet, Antwort per TTS vorgelesen (`VoicePanel.svelte`)
|
- **Voice-zu-Claude-Pipeline**: Spracheingabe wird transkribiert, an Claude gesendet, Antwort per TTS vorgelesen (`VoicePanel.svelte`)
|
||||||
|
|
@ -27,6 +33,8 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
||||||
- `lib.rs`: App-Lifecycle erweitert um Lock-Datei create/remove bei Start/Exit
|
- `lib.rs`: App-Lifecycle erweitert um Lock-Datei create/remove bei Start/Exit
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
- **Update-Fortschrittsbalken**: Erreicht jetzt visuell 100% vor der Bestätigungsmeldung (`update.rs`, `UpdateDialog.svelte`)
|
||||||
|
- **Mikrofon in Produktion**: GStreamer + PipeWire-Plugins fehlten im Nix-Wrapper, WebKitGTK konnte getUserMedia nicht nutzen (`nix/default.nix`)
|
||||||
- Updater konnte Binary ersetzen während App noch lief (kein Lock, kein Prozess-Check)
|
- Updater konnte Binary ersetzen während App noch lief (kein Lock, kein Prozess-Check)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
33
ROADMAP.md
33
ROADMAP.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Claude Desktop — Roadmap
|
# Claude Desktop — Roadmap
|
||||||
|
|
||||||
Stand: 20.04.2026
|
Stand: 21.04.2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -58,14 +58,17 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
|
||||||
|
|
||||||
**Ziel:** Dinge die Codium + Extension prinzipbedingt NICHT koennen.
|
**Ziel:** Dinge die Codium + Extension prinzipbedingt NICHT koennen.
|
||||||
|
|
||||||
| Feature | Datei(en) | Beschreibung |
|
| Feature | Datei(en) | Status |
|
||||||
|---------|-----------|--------------|
|
|---------|-----------|--------|
|
||||||
| Projekt-Wechsel | `context.rs`, UI | Ein Klick wechselt Projekt (CWD, CLAUDE.md, Context, KB-Filter) |
|
| Projekt-Wechsel | `context.rs`, UI | ⬜ Ein Klick wechselt Projekt (CWD, CLAUDE.md, Context, KB-Filter) |
|
||||||
| MCP-Hub nativ | `claude-bridge.js` | Alle MCP-Server direkt nutzbar (Docker, Forgejo, DB) ohne CLI-Umweg |
|
| MCP-Hub nativ | `claude-bridge.js` | ⬜ Alle MCP-Server direkt nutzbar (Docker, Forgejo, DB) ohne CLI-Umweg |
|
||||||
| Guard-Rails UI | `guard.rs`, `GuardPanel.svelte` | Live-Anzeige was Claude darf/nicht darf, Ein-Klick-Freigabe |
|
| Guard-Rails UI | `guard.rs`, `GuardPanel.svelte` | ⬜ Live-Anzeige was Claude darf/nicht darf, Ein-Klick-Freigabe |
|
||||||
| Persistent Memory | `memory.rs`, `db.rs` | Cross-Session Gedaechtnis — Claude erinnert sich an ALLES |
|
| Persistent Memory | `memory.rs`, `db.rs` | ⬜ Cross-Session Gedaechtnis — Claude erinnert sich an ALLES |
|
||||||
| Quick-Actions | `CommandPalette.svelte` | Ctrl+K oeffnet Palette: Deploy, Build, Test, Commit — ein Tastendruck |
|
| ✅ Quick-Actions | `QuickActions.svelte`, `ChatPanel.svelte` | Ctrl+K Palette: Deploy, Build, Test, Commit, Git, Navigation |
|
||||||
| Voice-Conversation | `voice.rs`, `ChatPanel.svelte` | Hands-free Loop: Sprechen → Claude antwortet → TTS → warten auf naechste Eingabe |
|
| ✅ Voice-Conversation | `voice.rs`, `VoicePanel.svelte` | Lokales Whisper STT + Piper TTS, VAD, Gespraechsmodus |
|
||||||
|
| ✅ Settings-Panel | `SettingsPanel.svelte` | VS-Code-artiges Layout mit Suche, Kategorien, Commands, Hooks |
|
||||||
|
| ✅ Chat-Detach | `chat_window.rs`, `+page.svelte` | Chat in separates Fenster herausloesen/zurueckholen |
|
||||||
|
| ✅ Aktivitaets-Phasen | `events.ts`, `ChatPanel.svelte` | 4 Phasen: Denkt/Streamt/Tool/Subagent statt nur "Denkt nach" |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -73,12 +76,12 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
|
||||||
|
|
||||||
**Ziel:** Unabhaengig von Cloud fuer Routine-Tasks.
|
**Ziel:** Unabhaengig von Cloud fuer Routine-Tasks.
|
||||||
|
|
||||||
| Feature | Datei(en) | Beschreibung |
|
| Feature | Datei(en) | Status |
|
||||||
|---------|-----------|--------------|
|
|---------|-----------|--------|
|
||||||
| Whisper.cpp lokal | `voice.rs` | STT ohne OpenAI-API, laeuft auf GPU |
|
| ✅ Whisper.cpp lokal | `voice.rs` | whisper-cli STT, Thorsten-DE Modell, kein OpenAI noetig |
|
||||||
| Piper-TTS lokal | `voice.rs` | Deutsche Stimme offline |
|
| ✅ Piper-TTS lokal | `voice.rs` | piper-tts mit thorsten_emotional (high), offline |
|
||||||
| Lokales Haiku-Equivalent | `claude-bridge.js` | Ollama/llama.cpp fuer simple Tasks (Commit-Messages, Uebersetzungen) |
|
| ⬜ Lokales Haiku-Equivalent | `claude-bridge.js` | Ollama/llama.cpp fuer simple Tasks (Commit-Messages, Uebersetzungen) |
|
||||||
| Offline-Queue | `session.rs` | Nachrichten queuen wenn kein Netz, spaeter absenden |
|
| ⬜ Offline-Queue | `session.rs` | Nachrichten queuen wenn kein Netz, spaeter absenden |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ let
|
||||||
libsoup_3
|
libsoup_3
|
||||||
at-spi2-atk
|
at-spi2-atk
|
||||||
openssl
|
openssl
|
||||||
|
# GStreamer + PipeWire fuer WebKitGTK Audio (Mikrofon, getUserMedia)
|
||||||
|
gst_all_1.gstreamer
|
||||||
|
gst_all_1.gst-plugins-base
|
||||||
|
gst_all_1.gst-plugins-good
|
||||||
|
gst_all_1.gst-plugins-bad
|
||||||
|
pipewire
|
||||||
|
alsa-lib
|
||||||
];
|
];
|
||||||
|
|
||||||
# GStreamer-Plugins fuer WebKitGTK (Audio/Video, WebRTC, Mikrofon via PipeWire)
|
# GStreamer-Plugins fuer WebKitGTK (Audio/Video, WebRTC, Mikrofon via PipeWire)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, type Message } from '$lib/stores/app';
|
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, type Message, type QuickAction } from '$lib/stores/app';
|
||||||
import { currentTool, processingPhase } from '$lib/stores/events';
|
import { currentTool, processingPhase } from '$lib/stores/events';
|
||||||
import { marked, type Tokens } from 'marked';
|
import { marked, type Tokens } from 'marked';
|
||||||
import { tick, onDestroy, onMount } from 'svelte';
|
import { tick, onDestroy, onMount } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import CommandPalette from './CommandPalette.svelte';
|
import CommandPalette from './CommandPalette.svelte';
|
||||||
|
import QuickActions from './QuickActions.svelte';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
let { detached = false }: { detached?: boolean } = $props();
|
let { detached = false }: { detached?: boolean } = $props();
|
||||||
|
|
@ -218,6 +219,9 @@
|
||||||
let audioChunks: Blob[] = [];
|
let audioChunks: Blob[] = [];
|
||||||
let levelAnimationFrame: number | null = null;
|
let levelAnimationFrame: number | null = null;
|
||||||
|
|
||||||
|
// Quick-Actions Palette (Ctrl+K)
|
||||||
|
let showQuickActions = $state(false);
|
||||||
|
|
||||||
// Slash-Command Autocomplete State
|
// Slash-Command Autocomplete State
|
||||||
let showCommandPalette = $state(false);
|
let showCommandPalette = $state(false);
|
||||||
let commandQuery = $state('');
|
let commandQuery = $state('');
|
||||||
|
|
@ -537,12 +541,33 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Quick-Action ausführen (Callback von QuickActions-Komponente)
|
||||||
|
function handleQuickAction(action: QuickAction) {
|
||||||
|
if (action.command) {
|
||||||
|
// Als Nachricht an Claude senden
|
||||||
|
$currentInput = action.command;
|
||||||
|
sendMessage();
|
||||||
|
} else if (action.id === 'settings') {
|
||||||
|
// Navigation: Settings-Tab aktivieren (Event an Hauptfenster)
|
||||||
|
emit('navigate-tab', { panel: 'right', tab: 'settings' });
|
||||||
|
} else if (action.id === 'monitor') {
|
||||||
|
emit('navigate-tab', { panel: 'middle', tab: 'monitor' });
|
||||||
|
} else if (action.id === 'voice-check') {
|
||||||
|
// Ergebnis als System-Message
|
||||||
|
invoke('check_voice_availability').then((result) => {
|
||||||
|
addMessage('system', `🎤 Voice-Status: ${JSON.stringify(result, null, 2)}`);
|
||||||
|
}).catch((err) => {
|
||||||
|
addMessage('system', `⚠️ Voice-Check fehlgeschlagen: ${err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Globale Keyboard Shortcuts
|
// Globale Keyboard Shortcuts
|
||||||
function handleGlobalKeydown(event: KeyboardEvent) {
|
function handleGlobalKeydown(event: KeyboardEvent) {
|
||||||
// Ctrl+K: Focus auf Input
|
// Ctrl+K: Quick-Actions Palette öffnen/schliessen
|
||||||
if (event.ctrlKey && event.key === 'k') {
|
if (event.ctrlKey && event.key === 'k') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
inputTextarea?.focus();
|
showQuickActions = !showQuickActions;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -893,7 +918,7 @@
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🤖</div>
|
<div class="empty-icon">🤖</div>
|
||||||
<p>Starte eine Konversation mit Claude.</p>
|
<p>Starte eine Konversation mit Claude.</p>
|
||||||
<p class="hint">Enter/Ctrl+Enter = Senden, Shift+Enter = Neue Zeile, Ctrl+K = Focus, Escape = Stopp</p>
|
<p class="hint">Enter/Ctrl+Enter = Senden, Shift+Enter = Neue Zeile, Ctrl+K = Quick-Actions, Escape = Stopp</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each $messages as message, index}
|
{#each $messages as message, index}
|
||||||
|
|
@ -1050,7 +1075,7 @@
|
||||||
bind:this={inputTextarea}
|
bind:this={inputTextarea}
|
||||||
bind:value={$currentInput}
|
bind:value={$currentInput}
|
||||||
onkeydown={handleKeydown}
|
onkeydown={handleKeydown}
|
||||||
placeholder={$isProcessing ? 'Nachricht eingeben — wird nach Antwort automatisch gesendet' : 'Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)'}
|
placeholder={$isProcessing ? 'Nachricht eingeben — wird nach Antwort automatisch gesendet' : 'Nachricht eingeben... (Ctrl+K = Quick-Actions, Ctrl+Enter = Senden)'}
|
||||||
disabled={isRecording}
|
disabled={isRecording}
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
@ -1083,6 +1108,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick-Actions Palette (Ctrl+K) -->
|
||||||
|
<QuickActions bind:visible={showQuickActions} onExecute={handleQuickAction} />
|
||||||
|
|
||||||
<!-- "Das merken" Dialog -->
|
<!-- "Das merken" Dialog -->
|
||||||
{#if rememberDialogOpen}
|
{#if rememberDialogOpen}
|
||||||
<div class="modal-backdrop" onclick={closeRememberDialog}>
|
<div class="modal-backdrop" onclick={closeRememberDialog}>
|
||||||
|
|
|
||||||
551
src/lib/components/QuickActions.svelte
Normal file
551
src/lib/components/QuickActions.svelte
Normal file
|
|
@ -0,0 +1,551 @@
|
||||||
|
<script lang="ts">
|
||||||
|
// Quick-Actions Palette (Ctrl+K)
|
||||||
|
// VS-Code-artige Kommandopalette für häufige Aktionen:
|
||||||
|
// Deploy, Build, Test, Commit, Projekt-Wechsel — ein Tastendruck.
|
||||||
|
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { type QuickAction } from '$lib/stores/app';
|
||||||
|
|
||||||
|
let {
|
||||||
|
visible = $bindable(false),
|
||||||
|
onExecute = (_action: QuickAction) => {}
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
onExecute: (action: QuickAction) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// Suchfeld
|
||||||
|
let searchQuery = $state('');
|
||||||
|
let selectedIndex = $state(0);
|
||||||
|
let searchInput: HTMLInputElement;
|
||||||
|
|
||||||
|
// Vordefinierte Quick-Actions
|
||||||
|
const actions: QuickAction[] = [
|
||||||
|
// === Build & Deploy ===
|
||||||
|
{
|
||||||
|
id: 'build',
|
||||||
|
label: 'Build starten',
|
||||||
|
description: 'Tauri-App kompilieren (nix-shell)',
|
||||||
|
icon: '🔨',
|
||||||
|
category: 'build',
|
||||||
|
shortcut: 'B',
|
||||||
|
command: '/build'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deploy',
|
||||||
|
label: 'Deployen',
|
||||||
|
description: 'Build + Push + CI/CD Pipeline starten',
|
||||||
|
icon: '🚀',
|
||||||
|
category: 'build',
|
||||||
|
shortcut: 'D',
|
||||||
|
command: '/deploy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'test',
|
||||||
|
label: 'Tests ausführen',
|
||||||
|
description: 'cargo test + npm test',
|
||||||
|
icon: '🧪',
|
||||||
|
category: 'build',
|
||||||
|
shortcut: 'T',
|
||||||
|
command: 'Führe alle Tests aus (cargo test und npm test)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lint',
|
||||||
|
label: 'Lint & Format',
|
||||||
|
description: 'Code prüfen und formatieren',
|
||||||
|
icon: '✨',
|
||||||
|
category: 'build',
|
||||||
|
command: 'Führe cargo clippy und npm run lint aus und behebe alle Warnungen'
|
||||||
|
},
|
||||||
|
|
||||||
|
// === Git ===
|
||||||
|
{
|
||||||
|
id: 'commit',
|
||||||
|
label: 'Commit erstellen',
|
||||||
|
description: 'Änderungen committen mit sinnvoller Message',
|
||||||
|
icon: '📝',
|
||||||
|
category: 'git',
|
||||||
|
shortcut: 'C',
|
||||||
|
command: 'Erstelle einen Commit für alle aktuellen Änderungen mit einer passenden Commit-Message'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'git-status',
|
||||||
|
label: 'Git Status',
|
||||||
|
description: 'Aktuellen Stand des Repos anzeigen',
|
||||||
|
icon: '📊',
|
||||||
|
category: 'git',
|
||||||
|
command: 'Zeige git status, git diff --stat und die letzten 5 Commits'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'git-push',
|
||||||
|
label: 'Git Push',
|
||||||
|
description: 'Commits zum Remote pushen',
|
||||||
|
icon: '⬆️',
|
||||||
|
category: 'git',
|
||||||
|
command: 'Pushe die aktuellen Commits zum Remote (git push)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'changelog',
|
||||||
|
label: 'Changelog aktualisieren',
|
||||||
|
description: 'CHANGELOG.md mit den letzten Änderungen aktualisieren',
|
||||||
|
icon: '📋',
|
||||||
|
category: 'git',
|
||||||
|
command: 'Aktualisiere die CHANGELOG.md mit den neuesten Änderungen seit dem letzten Eintrag'
|
||||||
|
},
|
||||||
|
|
||||||
|
// === Session ===
|
||||||
|
{
|
||||||
|
id: 'new-session',
|
||||||
|
label: 'Neue Session',
|
||||||
|
description: 'Neue Chat-Session starten',
|
||||||
|
icon: '➕',
|
||||||
|
category: 'session',
|
||||||
|
shortcut: 'N',
|
||||||
|
invoke: 'create_session'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compact',
|
||||||
|
label: 'Context komprimieren',
|
||||||
|
description: 'Alte Nachrichten zusammenfassen (spart Tokens)',
|
||||||
|
icon: '🗜️',
|
||||||
|
category: 'session',
|
||||||
|
command: '/compact'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'clear',
|
||||||
|
label: 'Chat leeren',
|
||||||
|
description: 'Alle Nachrichten in dieser Session löschen',
|
||||||
|
icon: '🗑️',
|
||||||
|
category: 'session',
|
||||||
|
command: '/clear'
|
||||||
|
},
|
||||||
|
|
||||||
|
// === Navigation ===
|
||||||
|
{
|
||||||
|
id: 'detach-chat',
|
||||||
|
label: 'Chat herauslösen',
|
||||||
|
description: 'Chat in separatem Fenster öffnen',
|
||||||
|
icon: '⧉',
|
||||||
|
category: 'navigation',
|
||||||
|
invoke: 'chat_window_open'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
label: 'Einstellungen',
|
||||||
|
description: 'Settings-Panel öffnen',
|
||||||
|
icon: '⚙️',
|
||||||
|
category: 'navigation',
|
||||||
|
command: '' // Wird über onExecute gehandhabt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'roadmap',
|
||||||
|
label: 'Roadmap zeigen',
|
||||||
|
description: 'Projekt-Roadmap und offene Tasks',
|
||||||
|
icon: '🗺️',
|
||||||
|
category: 'navigation',
|
||||||
|
command: 'Zeige die aktuelle Roadmap (ROADMAP.md) und markiere was abgeschlossen und was offen ist'
|
||||||
|
},
|
||||||
|
|
||||||
|
// === Voice ===
|
||||||
|
{
|
||||||
|
id: 'voice-check',
|
||||||
|
label: 'Voice-Status prüfen',
|
||||||
|
description: 'Whisper + Piper Verfügbarkeit testen',
|
||||||
|
icon: '🎤',
|
||||||
|
category: 'voice',
|
||||||
|
invoke: 'check_voice_availability'
|
||||||
|
},
|
||||||
|
|
||||||
|
// === Tools ===
|
||||||
|
{
|
||||||
|
id: 'kb-search',
|
||||||
|
label: 'Wissensbasis durchsuchen',
|
||||||
|
description: 'In der Claude-DB nach Einträgen suchen',
|
||||||
|
icon: '📚',
|
||||||
|
category: 'tools',
|
||||||
|
command: 'Durchsuche die Wissensbasis nach relevanten Einträgen zum aktuellen Projekt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'memory',
|
||||||
|
label: 'Gedächtnis anzeigen',
|
||||||
|
description: 'Was sich Claude gemerkt hat',
|
||||||
|
icon: '🧠',
|
||||||
|
category: 'tools',
|
||||||
|
command: '/memory'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'monitor',
|
||||||
|
label: 'System-Monitor',
|
||||||
|
description: 'Performance und API-Metriken anzeigen',
|
||||||
|
icon: '📊',
|
||||||
|
category: 'tools',
|
||||||
|
command: '' // Navigation
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Kategorie-Reihenfolge und Labels
|
||||||
|
const categoryLabels: Record<string, string> = {
|
||||||
|
build: '🔨 Build & Deploy',
|
||||||
|
git: '📦 Git',
|
||||||
|
session: '💬 Session',
|
||||||
|
navigation: '🧭 Navigation',
|
||||||
|
voice: '🎤 Sprache',
|
||||||
|
tools: '🛠️ Tools',
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryOrder = ['build', 'git', 'session', 'navigation', 'voice', 'tools'];
|
||||||
|
|
||||||
|
// Gefilterte Actions basierend auf Suche
|
||||||
|
let filtered = $derived.by(() => {
|
||||||
|
if (!searchQuery.trim()) return actions;
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
return actions.filter(a =>
|
||||||
|
a.label.toLowerCase().includes(q) ||
|
||||||
|
a.description.toLowerCase().includes(q) ||
|
||||||
|
a.category.includes(q) ||
|
||||||
|
(a.shortcut && a.shortcut.toLowerCase() === q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gruppiert nach Kategorie
|
||||||
|
let grouped = $derived.by(() => {
|
||||||
|
const groups: Record<string, QuickAction[]> = {};
|
||||||
|
for (const action of filtered) {
|
||||||
|
if (!groups[action.category]) {
|
||||||
|
groups[action.category] = [];
|
||||||
|
}
|
||||||
|
groups[action.category].push(action);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flache Liste für Keyboard-Navigation
|
||||||
|
let flatList = $derived.by(() => {
|
||||||
|
const flat: QuickAction[] = [];
|
||||||
|
for (const cat of categoryOrder) {
|
||||||
|
if (grouped[cat]) {
|
||||||
|
flat.push(...grouped[cat]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flat;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Index korrigieren bei Filter-Änderung
|
||||||
|
$effect(() => {
|
||||||
|
if (selectedIndex >= flatList.length) {
|
||||||
|
selectedIndex = Math.max(0, flatList.length - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus auf Suchfeld wenn sichtbar
|
||||||
|
$effect(() => {
|
||||||
|
if (visible) {
|
||||||
|
searchQuery = '';
|
||||||
|
selectedIndex = 0;
|
||||||
|
// Nächster Tick: Input fokussieren
|
||||||
|
setTimeout(() => searchInput?.focus(), 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false;
|
||||||
|
searchQuery = '';
|
||||||
|
selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeAction(action: QuickAction) {
|
||||||
|
close();
|
||||||
|
|
||||||
|
if (action.invoke) {
|
||||||
|
// Direkt Tauri-Command aufrufen
|
||||||
|
try {
|
||||||
|
const result = await invoke(action.invoke, action.invokeArgs || {});
|
||||||
|
// Ergebnis als System-Nachricht anzeigen wenn sinnvoll
|
||||||
|
if (action.id === 'voice-check' && result) {
|
||||||
|
onExecute(action);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Quick-Action '${action.id}' fehlgeschlagen:`, err);
|
||||||
|
}
|
||||||
|
} else if (action.command) {
|
||||||
|
// Als Chat-Nachricht senden
|
||||||
|
onExecute(action);
|
||||||
|
} else {
|
||||||
|
// Navigation oder spezielle Aktion
|
||||||
|
onExecute(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedIndex = selectedIndex <= 0 ? flatList.length - 1 : selectedIndex - 1;
|
||||||
|
scrollToSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedIndex = selectedIndex >= flatList.length - 1 ? 0 : selectedIndex + 1;
|
||||||
|
scrollToSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const action = flatList[selectedIndex];
|
||||||
|
if (action) {
|
||||||
|
executeAction(action);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToSelected() {
|
||||||
|
const el = document.querySelector('.qa-item.selected');
|
||||||
|
el?.scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backdrop-Klick schliesst
|
||||||
|
function handleBackdropClick(event: MouseEvent) {
|
||||||
|
if ((event.target as HTMLElement)?.classList.contains('qa-backdrop')) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
|
<div class="qa-backdrop" onclick={handleBackdropClick}>
|
||||||
|
<div class="qa-dialog" role="dialog" aria-label="Quick-Actions">
|
||||||
|
<div class="qa-search">
|
||||||
|
<span class="qa-search-icon">⚡</span>
|
||||||
|
<input
|
||||||
|
bind:this={searchInput}
|
||||||
|
bind:value={searchQuery}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
placeholder="Aktion suchen... (z.B. deploy, commit, build)"
|
||||||
|
type="text"
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<kbd class="qa-esc">Esc</kbd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qa-results">
|
||||||
|
{#if flatList.length === 0}
|
||||||
|
<div class="qa-empty">Keine Aktion gefunden für „{searchQuery}"</div>
|
||||||
|
{:else}
|
||||||
|
{#each categoryOrder as cat}
|
||||||
|
{#if grouped[cat]}
|
||||||
|
<div class="qa-category-label">{categoryLabels[cat]}</div>
|
||||||
|
{#each grouped[cat] as action}
|
||||||
|
{@const globalIdx = flatList.indexOf(action)}
|
||||||
|
<button
|
||||||
|
class="qa-item"
|
||||||
|
class:selected={globalIdx === selectedIndex}
|
||||||
|
onmouseenter={() => { selectedIndex = globalIdx; }}
|
||||||
|
onclick={() => executeAction(action)}
|
||||||
|
>
|
||||||
|
<span class="qa-icon">{action.icon}</span>
|
||||||
|
<div class="qa-text">
|
||||||
|
<span class="qa-label">{action.label}</span>
|
||||||
|
<span class="qa-desc">{action.description}</span>
|
||||||
|
</div>
|
||||||
|
{#if action.shortcut}
|
||||||
|
<kbd class="qa-shortcut">Alt+{action.shortcut}</kbd>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qa-footer">
|
||||||
|
<span>↑↓ Navigieren</span>
|
||||||
|
<span>Enter Ausführen</span>
|
||||||
|
<span>Esc Schliessen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.qa-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 15vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-dialog {
|
||||||
|
width: min(580px, 90vw);
|
||||||
|
background: var(--bg-secondary, #1e1e2e);
|
||||||
|
border: 1px solid var(--border, #333);
|
||||||
|
border-radius: var(--radius-lg, 12px);
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: qaSlideIn 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes qaSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border, #333);
|
||||||
|
background: var(--bg-primary, #13131d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-search-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-search input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--text-primary, #e0e0e0);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-search input::placeholder {
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-esc {
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--bg-tertiary, #2a2a3a);
|
||||||
|
border: 1px solid var(--border, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
font-family: inherit;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-results {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-category-label {
|
||||||
|
padding: 8px 16px 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
color: var(--text-primary, #e0e0e0);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-item:hover,
|
||||||
|
.qa-item.selected {
|
||||||
|
background: var(--bg-hover, rgba(255, 255, 255, 0.06));
|
||||||
|
border-left-color: var(--accent, #7c3aed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-desc {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-shortcut {
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--bg-tertiary, #2a2a3a);
|
||||||
|
border: 1px solid var(--border, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
font-family: inherit;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-empty {
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-top: 1px solid var(--border, #333);
|
||||||
|
background: var(--bg-primary, #13131d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-footer span {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -46,6 +46,19 @@ export interface Permission {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quick-Actions Typ (fuer QuickActions.svelte + ChatPanel.svelte)
|
||||||
|
export interface QuickAction {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
category: 'build' | 'git' | 'session' | 'navigation' | 'voice' | 'tools';
|
||||||
|
shortcut?: string;
|
||||||
|
command?: string; // Wird als Message an Claude gesendet
|
||||||
|
invoke?: string; // Tauri-Command direkt aufrufen
|
||||||
|
invokeArgs?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
export const agents = writable<Agent[]>([]);
|
export const agents = writable<Agent[]>([]);
|
||||||
export const toolCalls = writable<ToolCall[]>([]);
|
export const toolCalls = writable<ToolCall[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import SessionList from '$lib/components/SessionList.svelte';
|
import SessionList from '$lib/components/SessionList.svelte';
|
||||||
import ChatPanel from '$lib/components/ChatPanel.svelte';
|
import ChatPanel from '$lib/components/ChatPanel.svelte';
|
||||||
|
|
||||||
// Chat-Detach: Listener für Fenster-Events
|
// Chat-Detach + Quick-Actions Navigation: Listener für Fenster-Events
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Wenn das Chat-Fenster geschlossen wird → Chat wieder einblenden
|
// Wenn das Chat-Fenster geschlossen wird → Chat wieder einblenden
|
||||||
await listen('chat-reattached', () => {
|
await listen('chat-reattached', () => {
|
||||||
|
|
@ -18,6 +18,14 @@
|
||||||
await listen('chat-detached', () => {
|
await listen('chat-detached', () => {
|
||||||
$chatDetached = true;
|
$chatDetached = true;
|
||||||
});
|
});
|
||||||
|
// Quick-Actions Navigation: Tab in Panel aktivieren
|
||||||
|
await listen<{ panel: string; tab: string }>('navigate-tab', (event) => {
|
||||||
|
if (event.payload.panel === 'middle') {
|
||||||
|
activeMiddleTab = event.payload.tab;
|
||||||
|
} else if (event.payload.panel === 'right') {
|
||||||
|
activeRightTab = event.payload.tab;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sekundäre Panels: Lazy-Load bei erstem Tab-Wechsel
|
// Sekundäre Panels: Lazy-Load bei erstem Tab-Wechsel
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue