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.
226 lines
5.8 KiB
Svelte
226 lines
5.8 KiB
Svelte
<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>
|