All checks were successful
Build AppImage / build (push) Successful in 7m51s
- 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>
213 lines
5 KiB
Svelte
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>
|