All checks were successful
Build AppImage / build (push) Successful in 8m20s
Crash-Fix: - src/db.rs:801 panickte mit "byte index 240 is not a char boundary" mitten in einem ✅-Emoji → SIGABRT. Neues strutil-Modul mit safe_truncate()/safe_truncate_ellipsis() (5 Tests grün), an allen &s[..N]-Stellen in db/claude/knowledge/session/memory.rs eingebaut. - update.rs: Stale Lock-Files vom letzten Crash werden jetzt protokolliert ("🧹 Stale Lock-Datei aus vorherigem Crash gefunden"). Chat-Polish: - Input-Textfeld wird nach Senden zuverlässig geleert (Store-Reset + DOM-Reset + tick — Svelte 5 bind:value mit Auto-Subscription aktualisiert sonst nicht synchron). - ApprovalBar.svelte (NEU): Sticky-Bar überm Input mit klar beschrifteten Buttons "Übernehmen"/"Verwerfen" statt mehrdeutigem "Behalten/Zurueck". Bleibt sichtbar wenn der Chat scrollt. Klick auf Datei-Name scrollt zur Inline-Karte und blinkt sie. Shortcuts Ctrl+Enter/Ctrl+Backspace. - MessageList: Auto-Scroll trackt jetzt auch toolCalls.length und Status-Änderungen, plus ResizeObserver am Container. Smooth bei kleinen Distanzen, instant bei großen. - Streaming-Caret: pulsierender Block-Cursor mit Glow-Shadow. - Tool-Cards: Slide-In-Transition + Shimmer-Animation auf running. - WorkingIndicator: Verb passt sich an processingPhase an.
160 lines
4 KiB
Svelte
160 lines
4 KiB
Svelte
<script lang="ts">
|
|
// Animierter "Claude arbeitet"-Indikator unter der letzten Message.
|
|
// Rotiert ein zufaelliges deutsches Verb alle ~2.5s, daneben laeuft ein
|
|
// Braille-Spinner und ein Sekunden-Counter. Stil orientiert sich an
|
|
// Claude Code.
|
|
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { processingPhase, currentTool } from '$lib/stores/events';
|
|
|
|
// Allgemeine Verben fuer "denken/grübeln" — wenn keine Phase bekannt ist
|
|
const VERBS = [
|
|
'Denke nach',
|
|
'Gruebele',
|
|
'Bruete',
|
|
'Sinniere',
|
|
'Tueftle',
|
|
'Werkle',
|
|
'Stoebere',
|
|
'Knirsche',
|
|
'Bastle',
|
|
'Mische',
|
|
'Brodelt',
|
|
'Kluegele',
|
|
'Wuehle',
|
|
'Krame',
|
|
'Spinne',
|
|
'Schmiede',
|
|
'Pruefe',
|
|
'Lese',
|
|
'Verdaue',
|
|
'Sortiere',
|
|
'Sammle',
|
|
'Suche',
|
|
'Forsche',
|
|
'Faedele ein',
|
|
'Knete',
|
|
'Falte die Stirn',
|
|
'Klopfe ab',
|
|
'Lege Hand an',
|
|
];
|
|
|
|
// Pro Phase ein passender Text — Codium-Style "claude is thinking…"
|
|
function phaseVerb(phase: string, tool: string | null): string {
|
|
switch (phase) {
|
|
case 'thinking': return 'Denkt nach';
|
|
case 'streaming': return 'Schreibt';
|
|
case 'tool-use': return tool ? `Nutzt ${tool}` : 'Nutzt Tool';
|
|
case 'subagent': return 'Subagent arbeitet';
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
|
|
let randomVerb = $state(pickVerb());
|
|
let spinnerIdx = $state(0);
|
|
let elapsed = $state(0);
|
|
|
|
// Phase-bewusstes Verb: bevorzugt die spezifische Phase, sonst Random.
|
|
const verb = $derived.by(() => {
|
|
const pv = phaseVerb($processingPhase, $currentTool);
|
|
return pv || randomVerb;
|
|
});
|
|
|
|
let verbTimer: ReturnType<typeof setInterval> | null = null;
|
|
let spinTimer: ReturnType<typeof setInterval> | null = null;
|
|
let secondsTimer: ReturnType<typeof setInterval> | null = null;
|
|
let lastVerb = '';
|
|
|
|
function pickVerb(): string {
|
|
// Nicht zweimal hintereinander dasselbe Wort
|
|
let next = VERBS[Math.floor(Math.random() * VERBS.length)];
|
|
if (next === lastVerb && VERBS.length > 1) {
|
|
next = VERBS[(VERBS.indexOf(next) + 1) % VERBS.length];
|
|
}
|
|
lastVerb = next;
|
|
return next;
|
|
}
|
|
|
|
onMount(() => {
|
|
const start = Date.now();
|
|
verbTimer = setInterval(() => { randomVerb = pickVerb(); }, 2500);
|
|
spinTimer = setInterval(() => {
|
|
spinnerIdx = (spinnerIdx + 1) % SPINNER_FRAMES.length;
|
|
}, 90);
|
|
secondsTimer = setInterval(() => {
|
|
elapsed = Math.floor((Date.now() - start) / 1000);
|
|
}, 1000);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (verbTimer) clearInterval(verbTimer);
|
|
if (spinTimer) clearInterval(spinTimer);
|
|
if (secondsTimer) clearInterval(secondsTimer);
|
|
});
|
|
</script>
|
|
|
|
<div class="working" role="status" aria-live="polite">
|
|
<span class="spinner">{SPINNER_FRAMES[spinnerIdx]}</span>
|
|
<span class="verb">{verb}</span>
|
|
<span class="dots"><span>.</span><span>.</span><span>.</span></span>
|
|
{#if elapsed > 1}
|
|
<span class="elapsed">({elapsed}s)</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.working {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 16px 14px 52px;
|
|
font-size: 13px;
|
|
color: var(--vscode-descriptionForeground, #888);
|
|
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
opacity: 0;
|
|
animation: fade-in 200ms ease-out forwards;
|
|
}
|
|
|
|
.spinner {
|
|
color: var(--accent, var(--vscode-button-background, #007acc));
|
|
font-size: 15px;
|
|
line-height: 1;
|
|
display: inline-block;
|
|
min-width: 14px;
|
|
text-align: center;
|
|
}
|
|
|
|
.verb {
|
|
color: var(--text-primary, var(--vscode-foreground, #ddd));
|
|
font-style: italic;
|
|
min-width: 100px;
|
|
transition: opacity 200ms ease;
|
|
}
|
|
|
|
.dots {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
color: var(--text-primary, var(--vscode-foreground, #ddd));
|
|
}
|
|
.dots span {
|
|
animation: dot-pulse 1.4s ease-in-out infinite;
|
|
}
|
|
.dots span:nth-child(2) { animation-delay: 200ms; }
|
|
.dots span:nth-child(3) { animation-delay: 400ms; }
|
|
|
|
.elapsed {
|
|
color: var(--text-muted, #888);
|
|
font-size: 11px;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
@keyframes fade-in {
|
|
to { opacity: 1; }
|
|
}
|
|
@keyframes dot-pulse {
|
|
0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
|
|
30% { opacity: 1; transform: translateY(-2px); }
|
|
}
|
|
</style>
|