claude-desktop/src/lib/components/SettingsPanel.svelte
Eddy 506f1d3fdc
All checks were successful
Build AppImage / build (push) Successful in 7m52s
[appimage] Auto-Updater: Package Registry + update.json + Nix-Wrapper
- update.rs: Umstellung auf Package-Registry-Manifest mit SHA256-Verify,
  Basic-Auth, dev/APPIMAGE/Nix-Wrapper-Modus. Liest binary_filename
  im Nix-Modus (AppImage laeuft auf NixOS nicht)
- Nix-Wrapper-Paket (nix/default.nix): LD_LIBRARY_PATH-korrekter Launcher
  + Installer-Script, User-Home-Binary (writable fuer Auto-Update)
- CI laedt jetzt AppImage UND natives Binary + update.json v2
  (binary_filename/binary_sha256) in die Package Registry
- Svelte: Store-basierter Update-Trigger, manueller Check im
  Settings-Panel, "Kein Update"-Dialog-Variante, expectedSha256-Param
- install.sh: One-Click-Installer fuer NixOS (curl | bash)
- sha2-Dep fuer Integritaets-Check des Downloads

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:05:19 +02:00

504 lines
11 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { currentModel, agentMode, type AgentMode } from '$lib/stores/app';
import { updateCheckManual } from '$lib/stores/updateTrigger';
interface ModelInfo {
id: string;
name: string;
description: string;
}
let availableModels: ModelInfo[] = [];
let selectedModel = '';
let loading = true;
let saving = false;
let appVersion = '';
// Modell-Icons
const modelIcons: Record<string, string> = {
haiku: '🐦',
sonnet: '📝',
opus: '🎭',
};
// Preise pro 1M Token (ungefähre Werte)
const modelPrices: Record<string, { input: number; output: number }> = {
haiku: { input: 0.25, output: 1.25 },
sonnet: { input: 3, output: 15 },
opus: { input: 15, output: 75 },
};
// Agent-Modi
interface AgentModeInfo {
id: AgentMode;
name: string;
icon: string;
description: string;
}
const agentModes: AgentModeInfo[] = [
{
id: 'solo',
name: 'Solo',
icon: '🎯',
description: 'Main Agent macht alles selbst. Schnell für einfache Aufgaben.'
},
{
id: 'handlanger',
name: 'Handlanger',
icon: '👷',
description: 'Main denkt, Sub-Agents führen exakt aus. Gut für koordinierte Aufgaben.'
},
{
id: 'experten',
name: 'Experten',
icon: '🧠',
description: 'Jeder Agent denkt selbst. Ideal für komplexe, parallelisierbare Aufgaben.'
},
{
id: 'auto',
name: 'Auto',
icon: '🔄',
description: 'Modus wird automatisch basierend auf Aufgaben-Komplexität gewählt.'
}
];
async function loadSettings() {
try {
availableModels = await invoke('get_available_models');
const current: string = await invoke('get_current_model');
selectedModel = current;
$currentModel = current;
// Agent-Modus laden
const currentMode: string = await invoke('get_agent_mode');
$agentMode = currentMode as AgentMode;
// Version laden
appVersion = await invoke('get_current_version');
} catch (err) {
console.error('Fehler beim Laden:', err);
}
loading = false;
}
function triggerUpdateCheck() {
updateCheckManual.set(true);
}
async function changeMode(modeId: AgentMode) {
if (modeId === $agentMode) return;
try {
await invoke('set_agent_mode', { mode: modeId });
$agentMode = modeId;
} catch (err) {
console.error('Fehler beim Modus-Wechsel:', err);
}
}
async function changeModel(modelId: string) {
if (modelId === selectedModel) return;
saving = true;
try {
await invoke('set_model', { model: modelId });
selectedModel = modelId;
$currentModel = modelId;
} catch (err) {
console.error('Fehler beim Speichern:', err);
}
saving = false;
}
onMount(() => {
loadSettings();
});
function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}
</script>
<div class="settings-panel">
<div class="panel-header">
<h2>⚙️ Einstellungen</h2>
</div>
{#if loading}
<div class="loading">Lade Einstellungen...</div>
{:else}
<div class="settings-content">
<!-- Modell-Auswahl -->
<section class="settings-section">
<h3>🤖 Claude-Modell</h3>
<p class="section-hint">Wähle das Modell für deine Anfragen</p>
<div class="model-list">
{#each availableModels as model}
<button
class="model-card"
class:selected={selectedModel === model.id}
class:saving={saving && selectedModel !== model.id}
on:click={() => changeModel(model.id)}
disabled={saving}
>
<div class="model-header">
<span class="model-icon">{modelIcons[model.id] || '🤖'}</span>
<span class="model-name">{model.name}</span>
{#if selectedModel === model.id}
<span class="model-active">✓ Aktiv</span>
{/if}
</div>
<div class="model-description">{model.description}</div>
{#if modelPrices[model.id]}
<div class="model-pricing">
<span>Input: {formatPrice(modelPrices[model.id].input)}/1M</span>
<span>Output: {formatPrice(modelPrices[model.id].output)}/1M</span>
</div>
{/if}
</button>
{/each}
</div>
</section>
<!-- Agent-Modus -->
<section class="settings-section">
<h3>🤝 Agent-Modus</h3>
<p class="section-hint">Wie sollen komplexe Aufgaben bearbeitet werden?</p>
<div class="mode-list">
{#each agentModes as mode}
<button
class="mode-card"
class:selected={$agentMode === mode.id}
on:click={() => changeMode(mode.id)}
>
<div class="mode-header">
<span class="mode-icon">{mode.icon}</span>
<span class="mode-name">{mode.name}</span>
{#if $agentMode === mode.id}
<span class="mode-active">✓ Aktiv</span>
{/if}
</div>
<div class="mode-description">{mode.description}</div>
</button>
{/each}
</div>
<div class="mode-info">
{#if $agentMode === 'solo'}
<p>💡 <strong>Solo</strong> ist ideal für schnelle, einfache Aufgaben wie Typo-Fixes oder Code-Erklärungen.</p>
{:else if $agentMode === 'handlanger'}
<p>💡 <strong>Handlanger</strong> spart Context: Sub-Agents bekommen nur die nötigen Infos und liefern kompakte Zusammenfassungen.</p>
{:else if $agentMode === 'experten'}
<p>💡 <strong>Experten</strong> für komplexe Features: Research, Implement, Test und Review arbeiten parallel.</p>
{:else}
<p>💡 <strong>Auto</strong> analysiert die Aufgabe und wählt den passenden Modus automatisch.</p>
{/if}
</div>
</section>
<!-- Weitere Einstellungen -->
<section class="settings-section">
<h3>🎨 Darstellung</h3>
<div class="setting-row">
<span class="setting-label">Theme</span>
<span class="setting-value">AWL Dark (fest)</span>
</div>
</section>
<section class="settings-section">
<h3>📁 Pfade</h3>
<div class="setting-row">
<span class="setting-label">Arbeitsverzeichnis</span>
<span class="setting-value placeholder">Wird aus Session geladen</span>
</div>
</section>
<section class="settings-section">
<h3>🔄 Version & Updates</h3>
<div class="setting-row">
<span class="setting-label">Aktuelle Version</span>
<span class="setting-value">{appVersion || '—'}</span>
</div>
<div class="update-actions">
<button class="update-btn" on:click={triggerUpdateCheck}>
Nach Updates suchen
</button>
{#if appVersion === 'dev'}
<p class="dev-hint">
Entwicklungs-Build — Auto-Update ist deaktiviert.
Nur Pipeline-Builds können aktualisiert werden.
</p>
{/if}
</div>
</section>
</div>
{/if}
</div>
<style>
.settings-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
}
.panel-header h2 {
font-size: 0.875rem;
font-weight: 600;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
}
.settings-content {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
}
.settings-section {
margin-bottom: var(--spacing-lg);
}
.settings-section h3 {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.section-hint {
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
/* Modell-Karten */
.model-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.model-card {
width: 100%;
text-align: left;
padding: var(--spacing-md);
background: var(--bg-secondary);
border: 2px solid var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.model-card:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--text-secondary);
}
.model-card.selected {
border-color: var(--accent);
background: rgba(233, 69, 96, 0.1);
}
.model-card.saving {
opacity: 0.5;
cursor: wait;
}
.model-card:disabled {
cursor: not-allowed;
}
.model-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.model-icon {
font-size: 1.2rem;
}
.model-name {
font-weight: 600;
flex: 1;
}
.model-active {
font-size: 0.7rem;
padding: 2px 6px;
background: var(--accent);
color: white;
border-radius: var(--radius-sm);
}
.model-description {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.model-pricing {
display: flex;
gap: var(--spacing-md);
font-size: 0.7rem;
color: var(--text-secondary);
font-family: var(--font-mono);
}
/* Allgemeine Einstellungs-Zeilen */
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--bg-tertiary);
}
.setting-label {
font-size: 0.85rem;
}
.setting-value {
font-size: 0.85rem;
color: var(--text-secondary);
}
.setting-value.placeholder {
font-style: italic;
opacity: 0.6;
}
/* Agent-Modus Karten */
.mode-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-sm);
}
.mode-card {
text-align: left;
padding: var(--spacing-sm);
background: var(--bg-secondary);
border: 2px solid var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.mode-card:hover {
background: var(--bg-tertiary);
border-color: var(--text-secondary);
}
.mode-card.selected {
border-color: var(--accent);
background: rgba(233, 69, 96, 0.1);
}
.mode-header {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-xs);
}
.mode-icon {
font-size: 1rem;
}
.mode-name {
font-weight: 600;
font-size: 0.85rem;
flex: 1;
}
.mode-active {
font-size: 0.6rem;
padding: 1px 4px;
background: var(--accent);
color: white;
border-radius: var(--radius-sm);
}
.mode-description {
font-size: 0.7rem;
color: var(--text-secondary);
line-height: 1.3;
}
.mode-info {
margin-top: var(--spacing-md);
padding: var(--spacing-sm);
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: var(--radius-sm);
font-size: 0.75rem;
line-height: 1.4;
}
.mode-info p {
margin: 0;
}
.mode-info strong {
color: var(--accent);
}
/* Update-Sektion */
.update-actions {
margin-top: var(--spacing-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.update-btn {
align-self: flex-start;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.update-btn:hover {
background: var(--accent-hover);
}
.dev-hint {
margin: 0;
padding: var(--spacing-sm);
font-size: 0.75rem;
color: var(--text-secondary);
background: rgba(234, 179, 8, 0.08);
border: 1px solid rgba(234, 179, 8, 0.2);
border-radius: var(--radius-sm);
line-height: 1.4;
}
</style>