[appimage] UI-Polish: Icon, Stop-Button dezent, Chat-Queue, Update-Safety
All checks were successful
Build AppImage / build (push) Successful in 6m37s
- Neues Icon-Set (SVG-Quelle + gen-icon.sh): 32/64/128/256/512+@2x in depth=8 (Tauri-Tray erwartet 8-bit-RGBA, depth=16 crashte den Tray-Setup) - StopButton: Icon-only (⏹), Position Titlebar rechts, nur sichtbar wenn isProcessing aktiv. Kein full-width roter Balken im Footer mehr. - .footer.active-Farbwechsel entfernt — Footer bleibt neutral - Version-Badge in der Titlebar (v<APP_VERSION>) - Chat-Input-Queue: Single-Slot-Puffer. Beim Senden waehrend Processing wird die Nachricht gepuffert, Pill "Nachricht wartet..." erscheint, nach Ende der aktuellen Antwort wird automatisch abgeschickt. - Stop verwirft den gepufferten Slot (bewusster Abbruch). - apply_update: ELF-Header-Smoke-Test vor Rename. Kaputter Download oder falsche Architektur liefert Fehlerdialog statt zerschossene Installation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48
scripts/gen-icon.sh
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env bash
|
||||
# Erzeugt alle Icon-Groessen fuer Tauri aus scripts/icon.svg.
|
||||
# Braucht ImageMagick (auf Nix: via nix-shell automatisch verfuegbar).
|
||||
#
|
||||
# Aufruf: ./scripts/gen-icon.sh
|
||||
# Ergebnis: src-tauri/icons/{icon.png, 32x32.png, 128x128.png, 128x128@2x.png}
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
SVG="scripts/icon.svg"
|
||||
OUT="src-tauri/icons"
|
||||
|
||||
if [ ! -f "$SVG" ]; then
|
||||
echo "❌ $SVG nicht gefunden" >&2; exit 1
|
||||
fi
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# ImageMagick aus Nix ziehen falls nicht vorhanden
|
||||
if ! command -v magick >/dev/null && ! command -v convert >/dev/null; then
|
||||
echo "ℹ️ ImageMagick fehlt — ziehe aus nixpkgs"
|
||||
IM=$(nix-build '<nixpkgs>' -A imagemagick --no-out-link 2>/dev/null || true)
|
||||
[ -n "$IM" ] && export PATH="$IM/bin:$PATH" || { echo "❌ ImageMagick nicht bezogen"; exit 1; }
|
||||
fi
|
||||
|
||||
# Moderne ImageMagick nutzt `magick`, aeltere `convert`
|
||||
if command -v magick >/dev/null; then IM_CMD="magick"; else IM_CMD="convert"; fi
|
||||
|
||||
echo "Generiere Icons aus $SVG mit $IM_CMD..."
|
||||
|
||||
# WICHTIG: -depth 8 erzwingen — sonst erzeugt ImageMagick aus hoher SVG-Density
|
||||
# 16-bit-RGBA-PNGs. Tauris Tray-Icon-Crate erwartet 8-bit RGBA (4 Byte/Pixel).
|
||||
for size in 32 64 128 256 512; do
|
||||
$IM_CMD -background none -density 400 "$SVG" -resize "${size}x${size}" -depth 8 "$OUT/${size}x${size}.png"
|
||||
echo " ✓ $OUT/${size}x${size}.png"
|
||||
done
|
||||
|
||||
# Retina-Variante (Tauri-Konvention)
|
||||
$IM_CMD -background none -density 400 "$SVG" -resize 256x256 -depth 8 "$OUT/128x128@2x.png"
|
||||
echo " ✓ $OUT/128x128@2x.png"
|
||||
|
||||
# Haupt-Icon
|
||||
cp "$OUT/512x512.png" "$OUT/icon.png"
|
||||
echo " ✓ $OUT/icon.png (Haupt-Icon 512x512)"
|
||||
|
||||
echo
|
||||
echo "✅ Fertig. Icons in $OUT/:"
|
||||
ls -l "$OUT"/*.png
|
||||
44
scripts/icon.svg
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<!-- Claude Desktop Icon — AWL-Farbpalette -->
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="35%" r="80%">
|
||||
<stop offset="0%" stop-color="#2a2d3e"/>
|
||||
<stop offset="100%" stop-color="#151722"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ef4a69"/>
|
||||
<stop offset="100%" stop-color="#c93154"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix values="0 0 0 0 0.92 0 0 0 0 0.27 0 0 0 0 0.41 0 0 0 0.55 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Hintergrund mit abgerundeten Ecken (iOS/Android-Style) -->
|
||||
<rect x="0" y="0" width="512" height="512" rx="112" ry="112" fill="url(#bg)"/>
|
||||
|
||||
<!-- Subtiler Akzent-Ring -->
|
||||
<circle cx="256" cy="256" r="180" fill="none" stroke="rgba(233,69,96,0.18)" stroke-width="3"/>
|
||||
|
||||
<!-- Hauptmotiv: stilisiertes C als Chat/Claude-Symbol -->
|
||||
<g filter="url(#glow)">
|
||||
<path d="M 346 180
|
||||
A 100 100 0 1 0 346 332
|
||||
L 346 286
|
||||
A 60 60 0 1 1 346 226
|
||||
Z"
|
||||
fill="url(#accent)"/>
|
||||
</g>
|
||||
|
||||
<!-- Schatten-Ring zur Tiefenwirkung -->
|
||||
<circle cx="256" cy="256" r="100"
|
||||
fill="none"
|
||||
stroke="rgba(0,0,0,0.4)"
|
||||
stroke-width="2"
|
||||
transform="rotate(-15 256 256)"/>
|
||||
|
||||
<!-- Kleiner Chat-Punkt (Dialog-Andeutung) -->
|
||||
<circle cx="200" cy="256" r="10" fill="#ffffff" opacity="0.92"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src-tauri/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 104 B After Width: | Height: | Size: 73 KiB |
|
|
@ -269,6 +269,10 @@ pub async fn download_update(
|
|||
/// 2. Nix-Wrapper-Modus → `$CLAUDE_DESKTOP_NIX_WRAPPER=1` + `$CLAUDE_DESKTOP_BIN` zeigt
|
||||
/// auf ~/.local/share/claude-desktop/bin/claude-desktop (writable, siehe nix/default.nix)
|
||||
/// 3. Entwicklungs-Build → Fehlerhinweis mit Build-Anleitung
|
||||
///
|
||||
/// Vor dem Rename wird ein Smoke-Test durchgeführt: Die heruntergeladene Datei
|
||||
/// muss ein valides ELF-Binary sein (Magic-Bytes 0x7F 'ELF'). Das fängt korrupte
|
||||
/// Downloads und falsche Architekturen ab, bevor das funktionsfähige Binary ersetzt wird.
|
||||
#[tauri::command]
|
||||
pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), String> {
|
||||
// Modus bestimmen: AppImage > Nix-Wrapper > Dev
|
||||
|
|
@ -289,7 +293,34 @@ pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), St
|
|||
.to_string()
|
||||
})?;
|
||||
|
||||
// Backup-Pfad: gleiches Verzeichnis, Suffix .backup
|
||||
// === Smoke-Test: Ist die neue Datei ein valides ELF-Binary? ===
|
||||
// ELF-Magic: 0x7F 'E' 'L' 'F' (gilt auch fuer AppImages, die self-extracting ELFs sind)
|
||||
let meta = std::fs::metadata(&update_path)
|
||||
.map_err(|e| format!("Update-Datei nicht zugreifbar: {}", e))?;
|
||||
if !meta.is_file() {
|
||||
return Err("Update-Datei ist keine regulaere Datei — Abbruch, bestehende Installation unveraendert.".into());
|
||||
}
|
||||
if meta.len() < 4 {
|
||||
return Err(format!(
|
||||
"Update-Datei ist zu klein ({} Bytes) — vermutlich abgebrochener Download. Bestehende Installation unveraendert.",
|
||||
meta.len()
|
||||
));
|
||||
}
|
||||
{
|
||||
use std::io::Read;
|
||||
let mut magic = [0u8; 4];
|
||||
std::fs::File::open(&update_path)
|
||||
.and_then(|mut f| f.read_exact(&mut magic))
|
||||
.map_err(|e| format!("Update-Datei nicht lesbar: {} — bestehende Installation unveraendert.", e))?;
|
||||
if magic != [0x7F, 0x45, 0x4C, 0x46] {
|
||||
return Err(format!(
|
||||
"Update-Datei ist kein ausfuehrbares ELF-Binary (Magic: {:02X} {:02X} {:02X} {:02X}). Bestehende Installation unveraendert.",
|
||||
magic[0], magic[1], magic[2], magic[3]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// === Backup + Rename ===
|
||||
let backup_path = {
|
||||
let mut p = target.clone();
|
||||
let file_name = target
|
||||
|
|
@ -306,7 +337,7 @@ pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), St
|
|||
std::fs::rename(&update_path, &target).map_err(|e| {
|
||||
// Rollback
|
||||
std::fs::rename(&backup_path, &target).ok();
|
||||
format!("Update-Installation fehlgeschlagen ({}): {}", mode_label, e)
|
||||
format!("Update-Installation fehlgeschlagen ({}): {} — Backup wiederhergestellt.", mode_label, e)
|
||||
})?;
|
||||
|
||||
std::fs::remove_file(&backup_path).ok();
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@
|
|||
"active": true,
|
||||
"targets": ["appimage", "deb"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.png"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, type Message } from '$lib/stores/app';
|
||||
import { marked, type Tokens } from 'marked';
|
||||
import { tick, onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
|
@ -386,15 +386,49 @@
|
|||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleGlobalKeydown);
|
||||
|
||||
// Queue-Auto-Dispatch: sobald isProcessing von true auf false wechselt
|
||||
// und ein Puffer vorhanden ist, wird er automatisch abgeschickt.
|
||||
let lastProcessing = false;
|
||||
const unsubProcessing = isProcessing.subscribe((val) => {
|
||||
if (lastProcessing && !val) {
|
||||
const queued = get(queuedMessage);
|
||||
if (queued) {
|
||||
$queuedMessage = null;
|
||||
dispatchMessage(queued).catch((e) => console.error('Queue-Dispatch fehlgeschlagen:', e));
|
||||
}
|
||||
}
|
||||
lastProcessing = val;
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleGlobalKeydown);
|
||||
unsubProcessing();
|
||||
};
|
||||
});
|
||||
|
||||
function cancelQueued() {
|
||||
$queuedMessage = null;
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const text = $currentInput.trim();
|
||||
if (!text) return;
|
||||
|
||||
// Waehrend Claude antwortet: Single-Slot-Puffer statt Doppel-Send.
|
||||
// Der Subscriber weiter unten sendet den Puffer automatisch ab, sobald
|
||||
// isProcessing von true auf false wechselt.
|
||||
if ($isProcessing) {
|
||||
$queuedMessage = text;
|
||||
$currentInput = '';
|
||||
return;
|
||||
}
|
||||
|
||||
await dispatchMessage(text);
|
||||
}
|
||||
|
||||
// Den eigentlichen Send-Flow ausgelagert, damit er auch fuer die Queue genutzt wird.
|
||||
async function dispatchMessage(text: string) {
|
||||
// Auto-Session erstellen falls keine aktiv
|
||||
let sessionId = get(currentSessionId);
|
||||
if (!sessionId) {
|
||||
|
|
@ -742,11 +776,18 @@
|
|||
<span class="transcript-text">{liveTranscript}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $queuedMessage}
|
||||
<div class="queued-pill" title="Wird gesendet sobald Claude die aktuelle Antwort fertig hat">
|
||||
<span class="queued-icon">📬</span>
|
||||
<span class="queued-text">Nachricht wartet: „{$queuedMessage.slice(0, 80)}{$queuedMessage.length > 80 ? '…' : ''}"</span>
|
||||
<button class="queued-cancel" onclick={cancelQueued} aria-label="Wartende Nachricht verwerfen">✕</button>
|
||||
</div>
|
||||
{/if}
|
||||
<textarea
|
||||
bind:this={inputTextarea}
|
||||
bind:value={$currentInput}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)"
|
||||
placeholder={$isProcessing ? 'Nachricht wird nach aktueller Antwort gesendet...' : 'Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)'}
|
||||
disabled={isRecording}
|
||||
rows="3"
|
||||
></textarea>
|
||||
|
|
@ -1409,6 +1450,44 @@
|
|||
color: #ef4444;
|
||||
}
|
||||
|
||||
.queued-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
background: rgba(96, 165, 250, 0.08);
|
||||
border: 1px solid rgba(96, 165, 250, 0.25);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.queued-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.queued-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.queued-cancel {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 0 4px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.queued-cancel:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.transcript-icon {
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,56 +17,45 @@
|
|||
class:disabled
|
||||
on:click={handleClick}
|
||||
{disabled}
|
||||
aria-label="Alle Agents sofort stoppen"
|
||||
title="Stopp (Esc) — beendet aktive Agenten, Session bleibt erhalten"
|
||||
aria-label="Aktive Agents stoppen"
|
||||
>
|
||||
<span class="stop-icon">⏹</span>
|
||||
<span class="stop-text">STOPP — Alles sofort abbrechen</span>
|
||||
<span class="stop-hint">(Escape)</span>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.stop-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: #c53030;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #e94560;
|
||||
font-size: 0.95rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid rgba(197, 48, 48, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.stop-button:not(.disabled):hover {
|
||||
background: #b52828;
|
||||
color: white;
|
||||
border-color: rgba(197, 48, 48, 0.8);
|
||||
background: rgba(233, 69, 96, 0.12);
|
||||
border-color: rgba(233, 69, 96, 0.35);
|
||||
}
|
||||
|
||||
.stop-button:not(.disabled):active {
|
||||
background: #a02020;
|
||||
background: rgba(233, 69, 96, 0.22);
|
||||
}
|
||||
|
||||
.stop-button.disabled {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stop-icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.stop-hint {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.65;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ export const selectedAgentId = writable<string | null>(null);
|
|||
export const currentModel = writable('');
|
||||
export const currentSessionId = writable<string | null>(null);
|
||||
|
||||
// Single-Slot-Puffer: wenn eine Nachricht abgeschickt wird waehrend Claude noch
|
||||
// antwortet, wird sie hier gehalten und nach Ende der aktuellen Antwort
|
||||
// automatisch gesendet. Neuere Inhalte ueberschreiben einen wartenden Slot.
|
||||
export const queuedMessage = writable<string | null>(null);
|
||||
|
||||
// Agent-Modus für Multi-Agent-Architektur
|
||||
export type AgentMode = 'solo' | 'handlanger' | 'experten' | 'auto';
|
||||
export const agentMode = writable<AgentMode>('solo');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import '../app.css';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
|
||||
import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, queuedMessage, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
|
||||
import StopButton from '$lib/components/StopButton.svelte';
|
||||
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
||||
|
||||
|
|
@ -33,9 +33,19 @@
|
|||
rules_count: number;
|
||||
}
|
||||
|
||||
let appVersion = '';
|
||||
|
||||
onMount(async () => {
|
||||
await initEventListeners();
|
||||
|
||||
// App-Version aus Rust holen (wird von der Pipeline als APP_VERSION gesetzt)
|
||||
try {
|
||||
appVersion = await invoke('get_current_version');
|
||||
} catch (err) {
|
||||
console.warn('App-Version konnte nicht geladen werden:', err);
|
||||
appVersion = 'dev';
|
||||
}
|
||||
|
||||
// Aktuelles Modell aus Settings laden
|
||||
try {
|
||||
const model: string = await invoke('get_current_model');
|
||||
|
|
@ -112,6 +122,9 @@
|
|||
} catch (err) {
|
||||
console.error('Fehler beim Stoppen:', err);
|
||||
}
|
||||
// Wartende Nachricht verwerfen — der User hat bewusst abgebrochen,
|
||||
// die Queue soll nicht automatisch nachfeuern.
|
||||
$queuedMessage = null;
|
||||
$isProcessing = false;
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +165,9 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="titlebar-right">
|
||||
{#if $isProcessing}
|
||||
<StopButton on:click={handleStop} />
|
||||
{/if}
|
||||
<button
|
||||
class="teach-btn"
|
||||
title="Schulungsmodus (Präsentations-Fenster)"
|
||||
|
|
@ -162,6 +178,11 @@
|
|||
{#if $currentModel}
|
||||
<span class="model-badge">{$currentModel.replace('claude-', '').replace(/-\d{8}$/, '').replace(/(\D)-(\d)/g, '$1 $2').replace(/(\d)-(\d)/g, '$1.$2').replace(/\b[a-z]/g, c => c.toUpperCase())}</span>
|
||||
{/if}
|
||||
{#if appVersion}
|
||||
<span class="version-badge" class:dev={appVersion === 'dev'} title="App-Version">
|
||||
v{appVersion}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -169,9 +190,8 @@
|
|||
<slot />
|
||||
</main>
|
||||
|
||||
<!-- Footer: STOPP + Stats -->
|
||||
<footer class="footer" class:active={$isProcessing}>
|
||||
<StopButton on:click={handleStop} disabled={!$isProcessing} />
|
||||
<!-- Footer: nur Stats (Stop-Button ist in die Titlebar gewandert) -->
|
||||
<footer class="footer">
|
||||
<div class="footer-stats">
|
||||
{#if $stickyContextInfo?.loaded}
|
||||
<span class="context-badge" title="Sticky Context: {$stickyContextInfo.entries} Einträge, ~{$stickyContextInfo.estimatedTokens} Token">
|
||||
|
|
@ -267,6 +287,18 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.version-badge.dev {
|
||||
opacity: 0.4;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.titlebar-right {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
|
|
@ -286,18 +318,13 @@
|
|||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.footer.active {
|
||||
border-top: 2px solid var(--error);
|
||||
}
|
||||
|
||||
.footer-stats {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
|
|
|
|||