claude-desktop/src/lib/components/CommandPalette.svelte
Eddy 0a447591da
All checks were successful
Build AppImage / build (push) Successful in 7m51s
Phase 1.5: Aktivierung & Quick-Wins [appimage]
- KB-Hints werden automatisch in jeden Claude-Prompt injiziert
- SQL-Queries berücksichtigen jetzt Priority (DESC)
- Voice-zu-Claude-Pipeline: Sprache → Transkription → Claude → TTS
- Hook-System feuert echte Events (SessionStart, Pre/PostToolUse)
- Pattern-Detektion bei Tool-Fehlern aktiviert
- Slash-Command Autocomplete mit CommandPalette
- Updater abgesichert: Lock-Datei, Prozess-Guard, Bestätigungs-Dialog
- ROADMAP.md und CHANGELOG.md aktualisiert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:00:40 +02:00

213 lines
5 KiB
Svelte

<script lang="ts">
// Slash-Command Autocomplete-Dropdown
// Zeigt verfuegbare Commands wenn der User "/" tippt
import { invoke } from '@tauri-apps/api/core';
import { onMount } from 'svelte';
// Props (Svelte 5 Runes)
let {
query = '',
visible = false,
onSelect = (_cmd: { name: string; description: string; category: string }) => {}
}: {
query: string;
visible: boolean;
onSelect: (cmd: { name: string; description: string; category: string }) => void;
} = $props();
// Gecachte Command-Liste
let allCommands: Array<{ name: string; description: string; category: string; source: string }> = $state([]);
let selectedIndex = $state(0);
let loaded = $state(false);
// Gefilterte Commands basierend auf Query
let filtered = $derived.by(() => {
if (!allCommands.length) return [];
const q = query.toLowerCase();
const result = allCommands.filter(cmd =>
cmd.name.toLowerCase().startsWith(q) || cmd.name.toLowerCase().includes(q)
);
// Maximal 8 Eintraege anzeigen
return result.slice(0, 8);
});
// Selektionsindex zuruecksetzen wenn sich die Filter-Ergebnisse aendern
$effect(() => {
if (filtered.length > 0) {
// Index begrenzen falls die Liste kuerzer geworden ist
if (selectedIndex >= filtered.length) {
selectedIndex = 0;
}
} else {
selectedIndex = 0;
}
});
// Commands beim ersten Sichtbarwerden laden (einmalig)
$effect(() => {
if (visible && !loaded) {
loadCommands();
}
});
async function loadCommands() {
try {
const cmds = await invoke<typeof allCommands>('get_slash_commands');
allCommands = cmds;
loaded = true;
} catch (err) {
console.error('Slash-Commands laden fehlgeschlagen:', err);
}
}
// Keyboard-Navigation (wird vom Parent aufgerufen)
export function handleKey(event: KeyboardEvent): boolean {
if (!visible || !filtered.length) return false;
if (event.key === 'ArrowUp') {
event.preventDefault();
selectedIndex = selectedIndex <= 0 ? filtered.length - 1 : selectedIndex - 1;
return true;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
selectedIndex = selectedIndex >= filtered.length - 1 ? 0 : selectedIndex + 1;
return true;
}
if (event.key === 'Tab' || event.key === 'Enter') {
event.preventDefault();
const cmd = filtered[selectedIndex];
if (cmd) {
onSelect(cmd);
}
return true;
}
if (event.key === 'Escape') {
event.preventDefault();
onSelect({ name: '', description: '', category: '' }); // Signalisiert Schliessen
return true;
}
return false;
}
// Kategorie-Badge Farbe bestimmen
function categoryColor(cat: string): string {
switch (cat) {
case 'builtin': return 'var(--accent)';
case 'custom': return 'var(--success)';
case 'skill': return 'var(--warning)';
default: return 'var(--text-secondary)';
}
}
// Kategorie-Label
function categoryLabel(cat: string): string {
switch (cat) {
case 'builtin': return 'Built-in';
case 'custom': return 'Custom';
case 'skill': return 'Skill';
default: return cat;
}
}
</script>
{#if visible && filtered.length > 0}
<div class="command-palette" role="listbox" aria-label="Slash-Commands">
{#each filtered as cmd, i}
<button
class="command-item"
class:selected={i === selectedIndex}
role="option"
aria-selected={i === selectedIndex}
onmouseenter={() => { selectedIndex = i; }}
onclick={() => onSelect(cmd)}
>
<span class="command-name">/{cmd.name}</span>
<span class="command-desc">{cmd.description}</span>
<span class="command-badge" style="background: {categoryColor(cmd.category)}">
{categoryLabel(cmd.category)}
</span>
</button>
{/each}
</div>
{/if}
<style>
.command-palette {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
margin-bottom: 4px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
overflow: hidden;
z-index: 100;
max-height: 320px;
overflow-y: auto;
}
.command-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
background: transparent;
border: none;
border-bottom: 1px solid var(--bg-tertiary);
color: var(--text-primary);
font-size: 0.8rem;
cursor: pointer;
text-align: left;
transition: background 0.1s ease;
}
.command-item:last-child {
border-bottom: none;
}
.command-item:hover,
.command-item.selected {
background: var(--bg-hover);
}
.command-item.selected {
border-left: 2px solid var(--accent);
padding-left: calc(var(--spacing-md) - 2px);
}
.command-name {
font-family: var(--font-mono);
font-weight: 600;
color: var(--accent);
white-space: nowrap;
min-width: 80px;
}
.command-desc {
flex: 1;
color: var(--text-secondary);
font-size: 0.75rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.command-badge {
padding: 0.1rem 0.4rem;
border-radius: var(--radius-sm);
font-size: 0.6rem;
font-weight: 600;
color: white;
white-space: nowrap;
opacity: 0.85;
}
</style>