claude-desktop/src/lib/components/ToolCallCard.svelte
Eddy 79f4f9fb21
All checks were successful
Build AppImage / build (push) Successful in 8m20s
fix: UTF-8-Crash + Input-Reset + ApprovalBar + Scroll/Streaming-Polish [appimage]
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.
2026-04-27 20:55:08 +02:00

226 lines
5.8 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
// Inline Tool-Call-Karte fuer Assistant-Messages (Phase 8)
//
// Sieht aus wie die Tool-Karten in der Claude-Code-Extension fuer VS Code:
// [▾ 📖 Read · src/app.ts:45-80]
// <Inhalt>
//
// Inhalt wird per <slot /> von Spezialisierungen gefuellt
// (ToolCardRead/Edit/Bash/...). Die Basis kuemmert sich um Header,
// Collapse, Status-Animation, Akzentleiste links.
import { getToolMeta, getToolSubtitle } from '$lib/utils/toolCards';
import type { InlineToolCall } from '$lib/stores';
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
interface Props {
call: InlineToolCall;
// Wenn der aufrufende Component bewusst aufgeklappt starten will:
defaultOpen?: boolean;
}
let { call, defaultOpen }: Props = $props();
const meta = $derived(getToolMeta(call.tool));
const subtitle = $derived(getToolSubtitle(call.tool, call.input));
// Manueller Override des Auto-Collapse-Verhaltens
let userOverride = $state<boolean | null>(null);
function autoOpen(): boolean {
if (defaultOpen !== undefined) return defaultOpen;
if (call.status === 'running') return true;
if (call.status === 'error') return true;
return !meta.collapseWhenDone;
}
const isOpen = $derived(userOverride !== null ? userOverride : autoOpen());
function toggle() {
userOverride = !isOpen;
}
function statusClass(s: string): string {
if (s === 'running') return 'running';
if (s === 'error') return 'error';
return 'success';
}
</script>
<!-- Tool-Card: kompakte „Background-Aktion"-Pille. Bewusst dezenter als
Chat-Messages — kleinerer Font, monospaced, gedämpfte Farben.
Sobald done und nichts Spannendes drin → bleibt collapsed. -->
<div
class="tool-card {statusClass(call.status)}"
class:open={isOpen}
data-tool-id={call.id}
transition:slide={{ duration: 180, easing: quintOut }}
>
<button class="card-header" onclick={toggle} aria-expanded={isOpen}>
<span class="chevron" class:open={isOpen}></span>
<span class="icon">{meta.icon}</span>
<span class="tool-name">{meta.label}</span>
{#if subtitle}
<span class="subtitle">{subtitle}</span>
{/if}
<span class="status-spacer"></span>
{#if call.status === 'running'}
<span class="status-dots" aria-label="laeuft">
<span></span><span></span><span></span>
</span>
{:else if call.status === 'error'}
<span class="status-icon error" title="Fehler"></span>
{:else}
<span class="status-icon ok" title="erledigt"></span>
{/if}
</button>
{#if isOpen}
<div class="card-body">
<slot {call} />
</div>
{/if}
</div>
<style>
.tool-card {
margin: 3px 0;
font-size: 11.5px;
overflow: hidden;
border-radius: 4px;
border-left: 2px solid var(--vscode-input-border, #3c3c3c);
opacity: 0.78;
transition: opacity 0.15s ease;
position: relative;
}
.tool-card.running {
opacity: 1;
border-left-color: var(--vscode-progressBar-background, #3794ff);
}
/* Shimmer-Effekt auf laufenden Karten — sanftes Lauflicht ueber den
linken Rand. Suggestiert "lebendige Aktion", aehnlich Codium. */
.tool-card.running::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(
180deg,
transparent 0%,
var(--vscode-progressBar-background, #3794ff) 40%,
var(--vscode-progressBar-background, #3794ff) 60%,
transparent 100%
);
animation: shimmer-slide 1.4s ease-in-out infinite;
pointer-events: none;
}
@keyframes shimmer-slide {
0% { transform: translateY(-100%); opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { transform: translateY(100%); opacity: 0; }
}
.tool-card.error {
opacity: 1;
border-left-color: var(--vscode-errorForeground, #f48771);
}
.tool-card.open {
opacity: 1;
}
.tool-card:hover {
opacity: 1;
}
.card-header {
display: flex;
align-items: center;
width: 100%;
gap: 6px;
padding: 3px 8px;
background: transparent;
color: var(--vscode-descriptionForeground, #888);
text-align: left;
font-size: 11.5px;
line-height: 1.5;
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
min-height: 22px;
}
.card-header:hover {
background: var(--vscode-list-hoverBackground, rgba(255,255,255,0.04));
color: var(--vscode-editor-foreground, #ddd);
}
.chevron {
display: inline-block;
font-size: 12px;
width: 8px;
color: var(--vscode-descriptionForeground);
transition: transform 0.12s ease;
flex-shrink: 0;
}
.chevron.open {
transform: rotate(90deg);
}
.icon {
font-size: 11px;
line-height: 1;
opacity: 0.85;
}
.tool-name {
font-weight: 500;
color: var(--vscode-foreground, #ccc);
font-size: 11.5px;
}
.subtitle {
color: var(--vscode-descriptionForeground);
font-family: var(--font-mono);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 55%;
opacity: 0.85;
}
.status-spacer { flex: 1; }
.status-icon {
font-size: 10px;
font-weight: 700;
}
.status-icon.ok { color: var(--vscode-successForeground, #89d185); opacity: 0.7; }
.status-icon.error { color: var(--vscode-errorForeground, #f48771); }
.status-dots {
display: inline-flex;
gap: 3px;
}
.status-dots span {
width: 3px;
height: 3px;
background: var(--vscode-progressBar-background, #3794ff);
border-radius: 50%;
animation: dotPulse 1.2s ease-in-out infinite;
}
.status-dots span:nth-child(2) { animation-delay: 0.15s; }
.status-dots span:nth-child(3) { animation-delay: 0.3s; }
@keyframes dotPulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.85); }
40% { opacity: 1; transform: scale(1); }
}
.card-body {
padding: 6px 10px 8px 18px;
font-size: 11.5px;
font-family: var(--font-mono, monospace);
color: var(--vscode-descriptionForeground);
background: var(--vscode-input-background, rgba(0,0,0,0.15));
border-top: 1px solid var(--vscode-input-border, #3c3c3c);
}
</style>