feat: Quick-Actions (Ctrl+K) + GStreamer-Fix für Produktion [appimage]
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:
Eddy 2026-04-21 10:02:40 +02:00
parent e87ac9ffc1
commit 9c6026de40
7 changed files with 640 additions and 22 deletions

View file

@ -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)
--- ---

View file

@ -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 |
--- ---

View file

@ -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)

View file

@ -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}>

View 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>

View file

@ -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[]>([]);

View file

@ -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