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
|
||||
- **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`)
|
||||
- **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`)
|
||||
|
|
@ -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
|
||||
|
||||
### 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)
|
||||
|
||||
---
|
||||
|
|
|
|||
33
ROADMAP.md
33
ROADMAP.md
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
| Feature | Datei(en) | Beschreibung |
|
||||
|---------|-----------|--------------|
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Quick-Actions | `CommandPalette.svelte` | Ctrl+K oeffnet Palette: Deploy, Build, Test, Commit — ein Tastendruck |
|
||||
| Voice-Conversation | `voice.rs`, `ChatPanel.svelte` | Hands-free Loop: Sprechen → Claude antwortet → TTS → warten auf naechste Eingabe |
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| 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 |
|
||||
| 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 |
|
||||
| ✅ Quick-Actions | `QuickActions.svelte`, `ChatPanel.svelte` | Ctrl+K Palette: Deploy, Build, Test, Commit, Git, Navigation |
|
||||
| ✅ Voice-Conversation | `voice.rs`, `VoicePanel.svelte` | Lokales Whisper STT + Piper TTS, VAD, Gespraechsmodus |
|
||||
| ✅ Settings-Panel | `SettingsPanel.svelte` | VS-Code-artiges Layout mit Suche, Kategorien, Commands, Hooks |
|
||||
| ✅ 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.
|
||||
|
||||
| Feature | Datei(en) | Beschreibung |
|
||||
|---------|-----------|--------------|
|
||||
| Whisper.cpp lokal | `voice.rs` | STT ohne OpenAI-API, laeuft auf GPU |
|
||||
| Piper-TTS lokal | `voice.rs` | Deutsche Stimme offline |
|
||||
| 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 |
|
||||
| Feature | Datei(en) | Status |
|
||||
|---------|-----------|--------|
|
||||
| ✅ Whisper.cpp lokal | `voice.rs` | whisper-cli STT, Thorsten-DE Modell, kein OpenAI noetig |
|
||||
| ✅ 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) |
|
||||
| ⬜ Offline-Queue | `session.rs` | Nachrichten queuen wenn kein Netz, spaeter absenden |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ let
|
|||
libsoup_3
|
||||
at-spi2-atk
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
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 { marked, type Tokens } from 'marked';
|
||||
import { tick, onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import CommandPalette from './CommandPalette.svelte';
|
||||
import QuickActions from './QuickActions.svelte';
|
||||
|
||||
// Props
|
||||
let { detached = false }: { detached?: boolean } = $props();
|
||||
|
|
@ -218,6 +219,9 @@
|
|||
let audioChunks: Blob[] = [];
|
||||
let levelAnimationFrame: number | null = null;
|
||||
|
||||
// Quick-Actions Palette (Ctrl+K)
|
||||
let showQuickActions = $state(false);
|
||||
|
||||
// Slash-Command Autocomplete State
|
||||
let showCommandPalette = $state(false);
|
||||
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
|
||||
function handleGlobalKeydown(event: KeyboardEvent) {
|
||||
// Ctrl+K: Focus auf Input
|
||||
// Ctrl+K: Quick-Actions Palette öffnen/schliessen
|
||||
if (event.ctrlKey && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
inputTextarea?.focus();
|
||||
showQuickActions = !showQuickActions;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -893,7 +918,7 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">🤖</div>
|
||||
<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>
|
||||
{:else}
|
||||
{#each $messages as message, index}
|
||||
|
|
@ -1050,7 +1075,7 @@
|
|||
bind:this={inputTextarea}
|
||||
bind:value={$currentInput}
|
||||
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}
|
||||
rows="3"
|
||||
></textarea>
|
||||
|
|
@ -1083,6 +1108,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick-Actions Palette (Ctrl+K) -->
|
||||
<QuickActions bind:visible={showQuickActions} onExecute={handleQuickAction} />
|
||||
|
||||
<!-- "Das merken" Dialog -->
|
||||
{#if rememberDialogOpen}
|
||||
<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;
|
||||
}
|
||||
|
||||
// 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
|
||||
export const agents = writable<Agent[]>([]);
|
||||
export const toolCalls = writable<ToolCall[]>([]);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import SessionList from '$lib/components/SessionList.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 () => {
|
||||
// Wenn das Chat-Fenster geschlossen wird → Chat wieder einblenden
|
||||
await listen('chat-reattached', () => {
|
||||
|
|
@ -18,6 +18,14 @@
|
|||
await listen('chat-detached', () => {
|
||||
$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
|
||||
|
|
|
|||
Loading…
Reference in a new issue