[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
|
/// 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)
|
/// auf ~/.local/share/claude-desktop/bin/claude-desktop (writable, siehe nix/default.nix)
|
||||||
/// 3. Entwicklungs-Build → Fehlerhinweis mit Build-Anleitung
|
/// 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]
|
#[tauri::command]
|
||||||
pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), String> {
|
pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), String> {
|
||||||
// Modus bestimmen: AppImage > Nix-Wrapper > Dev
|
// Modus bestimmen: AppImage > Nix-Wrapper > Dev
|
||||||
|
|
@ -289,7 +293,34 @@ pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), St
|
||||||
.to_string()
|
.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 backup_path = {
|
||||||
let mut p = target.clone();
|
let mut p = target.clone();
|
||||||
let file_name = target
|
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| {
|
std::fs::rename(&update_path, &target).map_err(|e| {
|
||||||
// Rollback
|
// Rollback
|
||||||
std::fs::rename(&backup_path, &target).ok();
|
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();
|
std::fs::remove_file(&backup_path).ok();
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["appimage", "deb"],
|
"targets": ["appimage", "deb"],
|
||||||
"icon": [
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.png"
|
"icons/icon.png"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { emit } from '@tauri-apps/api/event';
|
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 { marked, type Tokens } from 'marked';
|
||||||
import { tick, onDestroy, onMount } from 'svelte';
|
import { tick, onDestroy, onMount } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
@ -386,15 +386,49 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', handleGlobalKeydown);
|
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 () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleGlobalKeydown);
|
window.removeEventListener('keydown', handleGlobalKeydown);
|
||||||
|
unsubProcessing();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function cancelQueued() {
|
||||||
|
$queuedMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = $currentInput.trim();
|
const text = $currentInput.trim();
|
||||||
if (!text) return;
|
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
|
// Auto-Session erstellen falls keine aktiv
|
||||||
let sessionId = get(currentSessionId);
|
let sessionId = get(currentSessionId);
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
|
|
@ -742,11 +776,18 @@
|
||||||
<span class="transcript-text">{liveTranscript}</span>
|
<span class="transcript-text">{liveTranscript}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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
|
<textarea
|
||||||
bind:this={inputTextarea}
|
bind:this={inputTextarea}
|
||||||
bind:value={$currentInput}
|
bind:value={$currentInput}
|
||||||
onkeydown={handleKeydown}
|
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}
|
disabled={isRecording}
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
@ -1409,6 +1450,44 @@
|
||||||
color: #ef4444;
|
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 {
|
.transcript-icon {
|
||||||
animation: pulse 1s ease-in-out infinite;
|
animation: pulse 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,56 +17,45 @@
|
||||||
class:disabled
|
class:disabled
|
||||||
on:click={handleClick}
|
on:click={handleClick}
|
||||||
{disabled}
|
{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-icon">⏹</span>
|
||||||
<span class="stop-text">STOPP — Alles sofort abbrechen</span>
|
|
||||||
<span class="stop-hint">(Escape)</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.stop-button {
|
.stop-button {
|
||||||
width: 100%;
|
display: inline-flex;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--spacing-xs);
|
width: 24px;
|
||||||
padding: var(--spacing-xs) var(--spacing-sm);
|
height: 24px;
|
||||||
background: #c53030;
|
padding: 0;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
background: transparent;
|
||||||
font-size: 0.8rem;
|
color: #e94560;
|
||||||
font-weight: 600;
|
font-size: 0.95rem;
|
||||||
letter-spacing: 0.02em;
|
border: 1px solid transparent;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
border: 1px solid rgba(197, 48, 48, 0.6);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-button:not(.disabled):hover {
|
.stop-button:not(.disabled):hover {
|
||||||
background: #b52828;
|
background: rgba(233, 69, 96, 0.12);
|
||||||
color: white;
|
border-color: rgba(233, 69, 96, 0.35);
|
||||||
border-color: rgba(197, 48, 48, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-button:not(.disabled):active {
|
.stop-button:not(.disabled):active {
|
||||||
background: #a02020;
|
background: rgba(233, 69, 96, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-button.disabled {
|
.stop-button.disabled {
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-color: var(--bg-tertiary);
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stop-icon {
|
.stop-icon {
|
||||||
font-size: 0.875rem;
|
line-height: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.stop-hint {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 400;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ export const selectedAgentId = writable<string | null>(null);
|
||||||
export const currentModel = writable('');
|
export const currentModel = writable('');
|
||||||
export const currentSessionId = writable<string | null>(null);
|
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
|
// Agent-Modus für Multi-Agent-Architektur
|
||||||
export type AgentMode = 'solo' | 'handlanger' | 'experten' | 'auto';
|
export type AgentMode = 'solo' | 'handlanger' | 'experten' | 'auto';
|
||||||
export const agentMode = writable<AgentMode>('solo');
|
export const agentMode = writable<AgentMode>('solo');
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
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 StopButton from '$lib/components/StopButton.svelte';
|
||||||
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
||||||
|
|
||||||
|
|
@ -33,9 +33,19 @@
|
||||||
rules_count: number;
|
rules_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let appVersion = '';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await initEventListeners();
|
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
|
// Aktuelles Modell aus Settings laden
|
||||||
try {
|
try {
|
||||||
const model: string = await invoke('get_current_model');
|
const model: string = await invoke('get_current_model');
|
||||||
|
|
@ -112,6 +122,9 @@
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Stoppen:', 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;
|
$isProcessing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +165,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-right">
|
<div class="titlebar-right">
|
||||||
|
{#if $isProcessing}
|
||||||
|
<StopButton on:click={handleStop} />
|
||||||
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="teach-btn"
|
class="teach-btn"
|
||||||
title="Schulungsmodus (Präsentations-Fenster)"
|
title="Schulungsmodus (Präsentations-Fenster)"
|
||||||
|
|
@ -162,6 +178,11 @@
|
||||||
{#if $currentModel}
|
{#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>
|
<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}
|
||||||
|
{#if appVersion}
|
||||||
|
<span class="version-badge" class:dev={appVersion === 'dev'} title="App-Version">
|
||||||
|
v{appVersion}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -169,9 +190,8 @@
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer: STOPP + Stats -->
|
<!-- Footer: nur Stats (Stop-Button ist in die Titlebar gewandert) -->
|
||||||
<footer class="footer" class:active={$isProcessing}>
|
<footer class="footer">
|
||||||
<StopButton on:click={handleStop} disabled={!$isProcessing} />
|
|
||||||
<div class="footer-stats">
|
<div class="footer-stats">
|
||||||
{#if $stickyContextInfo?.loaded}
|
{#if $stickyContextInfo?.loaded}
|
||||||
<span class="context-badge" title="Sticky Context: {$stickyContextInfo.entries} Einträge, ~{$stickyContextInfo.estimatedTokens} Token">
|
<span class="context-badge" title="Sticky Context: {$stickyContextInfo.entries} Einträge, ~{$stickyContextInfo.estimatedTokens} Token">
|
||||||
|
|
@ -267,6 +287,18 @@
|
||||||
font-weight: 600;
|
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 {
|
.titlebar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
|
|
@ -286,18 +318,13 @@
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xs);
|
justify-content: center;
|
||||||
padding: var(--spacing-xs) var(--spacing-md);
|
padding: var(--spacing-xs) var(--spacing-md);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer.active {
|
|
||||||
border-top: 2px solid var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-stats {
|
.footer-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
|
|
|
||||||