fix(v8.1): Kritische Bugs behoben — system_status, model-routing, UI
P0: - hermes_agent: system_status Key-Mismatch behoben (cpu_pct→cpu.percent, ram_used_gb→ram.used/1024³, gtt analog) — zeigt jetzt echte Werte statt '?' - guides-data: & in 4 Titel-Strings → & (Svelte text-node rendert keine HTML-Entities) P1: - hermes_agent: choose_model() um recherchier/such/vergleich/etc. erweitert; kein Downgrade wenn komplexes Modell bereits geladen - routers/hermes: asyncio.get_event_loop() → get_running_loop() (Whisper+TTS) - maintenance: llama-swap Update akzeptiert optionales sudo-PW via stdin; import os hinzugefügt (fix NameError in updates()); UpdateReq-Model - ServerPanel: Engine-Update fragt jetzt nach sudo-PW (analog OS-Update) - HermesPanel: Chat-History in sessionStorage (überlebt Reload); Stopp-Button während Hermes antwortet; Verlauf-löschen-Button - ConnectPanel: opencode.json → opencode.jsonc; provider → providers; LAN-IP Override (localStorage) für NPM-Proxy-Setups Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,17 +6,29 @@
|
||||
const s = $derived(statusStore.value)
|
||||
const allModels = $derived(((s?.models || []) as any[]).filter((m: any) => !m.incomplete))
|
||||
|
||||
const url = $derived(baseUrl())
|
||||
let selectedTool = $state('zed')
|
||||
let selectedModelIds = $state<string[]>([])
|
||||
let testResult = $state('')
|
||||
let testOk = $state(false)
|
||||
let selectedMemTool = $state('cline')
|
||||
const memBlock = $derived(memMcpBlock(selectedMemTool))
|
||||
let ipOverride = $state<string>(localStorage.getItem('mc_ip_override') || '')
|
||||
|
||||
function baseUrl() { return `http://${location.hostname}:8080/v1` }
|
||||
function isLanIp(h: string) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) }
|
||||
function behindProxy() { return location.protocol === 'https:' || !isLanIp(location.hostname) }
|
||||
function baseUrl() {
|
||||
const host = ipOverride.trim() || location.hostname
|
||||
return `http://${host}:8080/v1`
|
||||
}
|
||||
|
||||
const url = $derived(baseUrl())
|
||||
|
||||
function saveIpOverride() {
|
||||
const v = ipOverride.trim()
|
||||
if (v) localStorage.setItem('mc_ip_override', v)
|
||||
else localStorage.removeItem('mc_ip_override')
|
||||
}
|
||||
|
||||
const memBlock = $derived(memMcpBlock(selectedMemTool))
|
||||
|
||||
const activeIds = $derived(
|
||||
selectedModelIds.length ? selectedModelIds : allModels.map((m: any) => m.name)
|
||||
@@ -42,7 +54,7 @@
|
||||
const ctx = m.meta?.ctx || 131072
|
||||
return ` "${m.name}": { "name": "${m.name}", "contextWindowSize": ${ctx} }`
|
||||
}).join(',\n')
|
||||
return `"provider": {\n "llama-swap": {\n "npm": "@ai-sdk/openai-compatible",\n "name": "Bosgame (llama-swap)",\n "options": {\n "baseURL": "${url}",\n "apiKey": "local"\n },\n "models": {\n${modelMap}\n }\n }\n}`
|
||||
return `"providers": {\n "llama-swap": {\n "npm": "@ai-sdk/openai-compatible",\n "name": "Bosgame (llama-swap)",\n "options": {\n "baseURL": "${url}",\n "apiKey": "local"\n },\n "models": {\n${modelMap}\n }\n }\n}`
|
||||
}
|
||||
|
||||
function continueBlock(url: string, ids: string[]) {
|
||||
@@ -97,10 +109,16 @@
|
||||
<div class="card-h"><h3>1 · Verbindung prüfen</h3></div>
|
||||
<div class="card-sub">Stelle sicher, dass die LLM-Engine erreichbar ist, bevor du Tools konfigurierst.</div>
|
||||
<label>Engine-Adresse (für alle Tools)</label>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:4px">
|
||||
<input class="mono-sm" readonly value={url} style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => { const inp = (e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement; copyVal(inp) }}>Kopieren</button>
|
||||
</div>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<input class="mono-sm" placeholder="LAN-IP überschreiben, z.B. 192.168.178.151 (leer = auto)"
|
||||
bind:value={ipOverride}
|
||||
oninput={saveIpOverride}
|
||||
style="flex:1;margin:0;font-size:12px;opacity:.7">
|
||||
</div>
|
||||
{#if behindProxy()}
|
||||
<div class="alert warn" style="margin:0 0 12px">
|
||||
<span class="a-dot"></span>
|
||||
@@ -254,14 +272,15 @@
|
||||
<div class="hint">Danach im Agent-Panel das Modell <b>bosgame / {firstModel}</b> auswählen.</div>
|
||||
{:else if selectedTool === 'opencode'}
|
||||
{@const block = openCodeBlock(url, activeIds)}
|
||||
<p>Datei anlegen oder öffnen:</p>
|
||||
<p class="mono-sm" style="line-height:1.6">Windows: <code>%USERPROFILE%\.config\opencode\opencode.json</code></p>
|
||||
<div class="alert warn" style="margin:8px 0 12px;padding:10px 14px"><span class="a-dot"></span><span>Nur den <code>"provider"</code>-Block einfügen. Falls bereits ein <code>"llama-swap"</code>-Eintrag vorhanden: <b>vorher löschen</b>, sonst erscheinen Modelle doppelt.</span></div>
|
||||
<p>Datei anlegen oder öffnen (OpenCode Desktop nutzt <b>.jsonc</b> — JSON mit Kommentaren):</p>
|
||||
<p class="mono-sm" style="line-height:1.6">Windows: <code>%USERPROFILE%\.config\opencode\opencode.jsonc</code></p>
|
||||
<div class="alert warn" style="margin:8px 0 12px;padding:10px 14px"><span class="a-dot"></span><span>Den <code>"providers"</code>-Block in die bestehende <code>opencode.jsonc</code> einfügen (nicht die ganze Datei ersetzen). Falls bereits ein <code>"llama-swap"</code>-Eintrag existiert: <b>vorher löschen</b>.</span></div>
|
||||
<div style="position:relative">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{block}</code></div>
|
||||
</div>
|
||||
<p style="margin-top:12px">Danach in OpenCode <code>/models</code> → <b>llama-swap/{firstModel}</b> wählen.</p>
|
||||
<div class="hint">OpenCode Desktop: <a href="https://opencode.ai" target="_blank" rel="noopener">opencode.ai</a></div>
|
||||
{:else if selectedTool === 'cline'}
|
||||
<p>In Cline/Cursor den Provider <b>„OpenAI Compatible"</b> wählen:</p>
|
||||
<label>Basis-URL</label>
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
type MsgRole = 'user' | 'hermes' | 'info' | 'tool' | 'error'
|
||||
type Msg = { role: MsgRole; content: string; tool?: string; args?: any }
|
||||
|
||||
const SESSION_KEY = 'hermes_chat_history'
|
||||
|
||||
// ---- State ----
|
||||
let messages = $state<Msg[]>([])
|
||||
let messages = $state<Msg[]>((() => {
|
||||
try { return JSON.parse(sessionStorage.getItem(SESSION_KEY) || '[]') } catch { return [] }
|
||||
})())
|
||||
let input = $state('')
|
||||
let thinking = $state(false)
|
||||
let stopping = $state(false)
|
||||
let voiceOn = $state(false)
|
||||
let recording = $state(false)
|
||||
let status = $state<any>(null)
|
||||
@@ -18,6 +23,11 @@
|
||||
let ws = $state<WebSocket | null>(null)
|
||||
let currentBot = $state('') // aktuell gestreamter Hermes-Token
|
||||
|
||||
// Nachrichten in sessionStorage sichern (lebt nur für aktive Browser-Tab-Session)
|
||||
$effect(() => {
|
||||
try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(messages)) } catch {}
|
||||
})
|
||||
|
||||
// ---- WebSocket ----
|
||||
function connect() {
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
@@ -56,6 +66,7 @@
|
||||
messages = [...messages, { role: 'error', content: m.content }]
|
||||
} else if (m.type === 'done') {
|
||||
thinking = false
|
||||
stopping = false
|
||||
currentBot = ''
|
||||
if (voiceOn) speakLast()
|
||||
}
|
||||
@@ -69,6 +80,18 @@
|
||||
ws = sock
|
||||
}
|
||||
|
||||
// ---- Stopp ----
|
||||
function stopGeneration() {
|
||||
stopping = true
|
||||
thinking = false
|
||||
if (currentBot) {
|
||||
messages = [...messages.slice(0, -1),
|
||||
{ role: 'hermes', content: currentBot + ' ✋' }]
|
||||
}
|
||||
currentBot = ''
|
||||
ws?.close() // onclose reconnects nach 3s automatisch
|
||||
}
|
||||
|
||||
// ---- Senden ----
|
||||
async function send() {
|
||||
const text = input.trim()
|
||||
@@ -76,9 +99,17 @@
|
||||
messages = [...messages, { role: 'user', content: text }]
|
||||
input = ''
|
||||
currentBot = ''
|
||||
stopping = false
|
||||
ws.send(JSON.stringify({ message: text }))
|
||||
}
|
||||
|
||||
// ---- Verlauf löschen ----
|
||||
function clearHistory() {
|
||||
messages = []
|
||||
currentBot = ''
|
||||
try { sessionStorage.removeItem(SESSION_KEY) } catch {}
|
||||
}
|
||||
|
||||
// ---- Voice: Aufnahme ----
|
||||
let mediaRecorder: MediaRecorder | null = null
|
||||
let audioChunks: Blob[] = []
|
||||
@@ -433,15 +464,31 @@
|
||||
{recording ? '⏹' : '🎤'}
|
||||
</button>
|
||||
|
||||
<!-- Senden-Button -->
|
||||
<button
|
||||
class="primary"
|
||||
style="padding:10px 18px;flex-shrink:0"
|
||||
disabled={!input.trim() || !ws || ws.readyState !== 1}
|
||||
onclick={send}>
|
||||
Senden
|
||||
</button>
|
||||
{#if thinking || stopping}
|
||||
<!-- Stopp-Button während Hermes antwortet -->
|
||||
<button class="ghost" style="padding:10px 18px;flex-shrink:0;border-color:var(--err);color:var(--err)"
|
||||
onclick={stopGeneration}>
|
||||
⏹ Stopp
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Senden-Button -->
|
||||
<button
|
||||
class="primary"
|
||||
style="padding:10px 18px;flex-shrink:0"
|
||||
disabled={!input.trim() || !ws || ws.readyState !== 1}
|
||||
onclick={send}>
|
||||
Senden
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Fußzeile: Verlauf löschen -->
|
||||
{#if messages.length > 0 && !thinking}
|
||||
<div style="border-top:1px solid rgba(255,255,255,.04);padding:6px 16px;display:flex;justify-content:flex-end">
|
||||
<button class="ghost" style="font-size:11px;padding:2px 8px;color:var(--mut)"
|
||||
onclick={clearHistory}>Verlauf löschen</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -74,7 +74,9 @@
|
||||
|
||||
async function engineUpdate() {
|
||||
if (!await confirmModal({ title: 'LLM-Engine aktualisieren?', body: 'Lädt die neueste llama.cpp-ROCm-Version und installiert sie. Die Engine ist während des Updates kurz nicht erreichbar.' })) return
|
||||
try { const r = await api('/api/update', { method: 'POST' }); toast('LLM-Engine-Update läuft.'); watchJob(r.job_id, 'LLM-Engine-Update') }
|
||||
const pwd = await promptModal({ title: 'sudo-Passwort', body: 'Für das Engine-Update wird einmalig dein sudo-Passwort gebraucht (für den Update-Befehl).', placeholder: 'sudo-Passwort', password: true, confirmLabel: 'Update starten' })
|
||||
if (pwd === null) return
|
||||
try { const r = await api('/api/update', { method: 'POST', body: JSON.stringify({ password: pwd }) }); toast('LLM-Engine-Update läuft.'); watchJob(r.job_id, 'LLM-Engine-Update') }
|
||||
catch (e: any) { toast(e.message, true) }
|
||||
}
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ export const GUIDE_GROUPS: GuideGroup[] = [
|
||||
<p><b>Merksatz:</b> Fehlt dem Agenten eine <em>Fähigkeit</em> → MCP.
|
||||
Soll er etwas nur <em>besser machen</em> → Skill (nächster Abschnitt).</p>`],
|
||||
|
||||
["Skills & Rules — Agenten Gewohnheiten beibringen", `
|
||||
["Skills & Rules — Agenten Gewohnheiten beibringen", `
|
||||
<p><b>Skills</b> (auch <em>Rules</em>, <em>AGENTS.md</em> oder <em>Instructions</em> genannt)
|
||||
sind kurze Texte, die dem Agenten beibringen wie er in deinem Projekt arbeiten soll —
|
||||
immer, ohne dass du es jedes Mal neu sagen musst.</p>
|
||||
@@ -418,7 +418,7 @@ Bevor du Dateien änderst: erkläre kurz was du vorhast.</pre>
|
||||
kritische Probleme (Prompt Injection, exponierte Secrets). Regel: Skill-Datei kurz lesen bevor du sie einsetzt.</p>
|
||||
<p style="margin-top:8px">Oder einfach fragen: <em>„Schreib mir eine AGENTS.md für ein React + TypeScript Projekt mit Tailwind."</em></p>`],
|
||||
|
||||
["Die Persona — Rolle, Beziehung & Onboarding", `
|
||||
["Die Persona — Rolle, Beziehung & Onboarding", `
|
||||
<p>Eine Persona hat <b>zwei Dimensionen</b> — beide sind wichtig:</p>
|
||||
<div class="tut-table">
|
||||
<div class="tut-row">
|
||||
@@ -503,7 +503,7 @@ Wenn du alle Antworten hast, erstelle daraus:
|
||||
</div>
|
||||
<p class="tut-sources" style="margin-top:10px">Fertige Personas: <a href="https://skills.sh" target="_blank" rel="noopener">skills.sh</a> · <a href="https://cursor.directory" target="_blank" rel="noopener">cursor.directory</a></p>`],
|
||||
|
||||
["AGENTS.MD & SAVEPOINT.MD — Gedächtnis für den Agenten", `
|
||||
["AGENTS.MD & SAVEPOINT.MD — Gedächtnis für den Agenten", `
|
||||
<p>Jedes Mal wenn du einen neuen Chat startest, weiß der Agent <b>nichts</b> über dein Projekt.
|
||||
Diese zwei Dateien lösen das Problem.</p>
|
||||
<p><b>AGENTS.md ist 2026 der plattformübergreifende Standard</b> — nativ gelesen von:
|
||||
@@ -593,7 +593,7 @@ src/
|
||||
{
|
||||
label: 'Wenn etwas nicht klappt',
|
||||
tutorials: [
|
||||
["Häufige Probleme & Lösungen", `
|
||||
["Häufige Probleme & Lösungen", `
|
||||
<div class="tut-table">
|
||||
<div class="tut-row">
|
||||
<div class="tut-cell-hd">🔴 Engine offline, rotes Licht in der Topbar</div>
|
||||
|
||||
Reference in New Issue
Block a user