Skills-Ansicht: Umbenennung + aktiv/inaktiv + Karten-Ergebnisse im Auftragsbuch

User-Feedback 15.07.:
1) 'Von allein' heisst jetzt 'Skills' (id #eigenleben bleibt, Links halten).
2) Jeder Skill traegt aktiv/inaktiv - Quelle ist Hermes' EIGENE Wahrheit
   (hermes_cli.banner.get_available_skills via Hermes-venv, dieselbe Logik
   wie das eingebaute WebUI: Plattform/Voraussetzungen/Config). Bewusst NUR
   die Datenquelle wiederverwendet, nicht deren UI - Skill-VERWALTUNG bleibt
   im Hermes-GUI-Tab, alles andere waere Bloat/Doppelbau.
3) Auftragsbuch: fertige Queue-Karten sind aufklappbar und zeigen das
   ERGEBNIS des Workers (kanban_complete-Summary, lazy via
   GET /api/ideen/{id}/ergebnis) - 'Dinge werden getestet, ich sehe aber
   das Ergebnis nicht' ist damit zu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-15 14:21:46 +02:00
parent bf9fd808d8
commit fdabe1768e
30 changed files with 197 additions and 48 deletions
+8
View File
@@ -507,6 +507,13 @@ export interface IdeenResp {
fehler?: string
}
// Ergebnis einer fertigen Queue-Aufgabe (Abschluss-Zusammenfassung des Workers)
export interface IdeenErgebnis {
available: boolean
summary: string
result: string
}
// ── Chronik (Timeline aus dem Melde-Briefkasten) ─────────────────────────────
export interface ChronikItem {
id: number
@@ -525,6 +532,7 @@ export interface EigenlebenSkill {
id: string
name: string
kategorie: string | null
aktiv: boolean | null // null = nicht ermittelbar (Dev-Modus)
beschreibung: string
version: string
autor: string
+1 -1
View File
@@ -29,7 +29,7 @@ export interface NavItem {
export const NAV: NavItem[] = [
{ id: "dashboard", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
{ id: "auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
{ id: "eigenleben", label: "Von allein", hint: "Was die Box selbst gebaut hat — Skills & Bilanz", icon: Sparkles, group: "Operativ" },
{ id: "eigenleben", label: "Skills", hint: "Fähigkeiten der Box — aktiv/inaktiv, Herkunft & Vorschlags-Bilanz", icon: Sparkles, group: "Operativ" },
{ id: "chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain, group: "Wissen" },
{ id: "wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
+45 -6
View File
@@ -1,9 +1,9 @@
import { useState } from "react"
import { useEffect, useState } from "react"
import {
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, Send, HelpCircle,
ChevronDown, ChevronRight, Clock, ClipboardCheck, Hammer, RefreshCw, Lightbulb, Send, HelpCircle,
} from "lucide-react"
import { api, type AuftragItem, type IdeenItem, type SkillKandidat } from "@/lib/api"
import { api, type AuftragItem, type IdeenErgebnis, type IdeenItem, type SkillKandidat } from "@/lib/api"
import { useAuftragsbuch, useIdeen, useIdeenLog, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils"
@@ -259,7 +259,9 @@ function IdeenSection() {
{items.map((it: IdeenItem) => {
const ui = IDEEN_STATE[it.status] ?? { label: it.status, cls: "text-muted-foreground border-border/60 bg-background/30" }
const isOpen = openLog === it.id
const canOpen = it.status === "triage" || it.status === "running"
// Aufklappbar: laufende Karten zeigen das Live-Log, FERTIGE das Ergebnis
// (User-Wunsch 15.07.: „Dinge werden getestet, ich sehe das Ergebnis nicht").
const canOpen = it.status === "triage" || it.status === "running" || it.status === "done"
const blocked = it.status === "blocked"
const answering = !!busy["ans:" + it.id]
return (
@@ -283,7 +285,7 @@ function IdeenSection() {
{canOpen && (
<button
onClick={() => setOpenLog(isOpen ? null : it.id)}
title="Worker-Log anzeigen"
title={it.status === "done" ? "Ergebnis anzeigen" : "Worker-Log anzeigen"}
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer">
{isOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
</button>
@@ -320,7 +322,7 @@ function IdeenSection() {
</div>
)}
{isOpen && canOpen && (
{isOpen && canOpen && it.status !== "done" && (
<div className="ml-2 -mt-1">
<div className="rounded-lg border border-border/50 bg-background/50 p-2">
{logEnabled && logData.isLoading ? (
@@ -337,6 +339,8 @@ function IdeenSection() {
</div>
</div>
)}
{isOpen && it.status === "done" && <ErgebnisPanel id={it.id} />}
</li>
)
})}
@@ -351,6 +355,41 @@ function IdeenSection() {
)
}
// Ergebnis einer FERTIGEN Queue-Aufgabe: die Abschluss-Zusammenfassung des Workers
// (kanban_complete). Lazy — wird erst beim Aufklappen geladen, ändert sich nie mehr.
function ErgebnisPanel({ id }: { id: string }) {
const [erg, setErg] = useState<IdeenErgebnis | null>(null)
const [fehler, setFehler] = useState(false)
useEffect(() => {
let weg = false
api<IdeenErgebnis>(`/api/ideen/${id}/ergebnis`)
.then((r) => { if (!weg) setErg(r) })
.catch(() => { if (!weg) setFehler(true) })
return () => { weg = true }
}, [id])
const text = erg ? (erg.summary || erg.result) : ""
return (
<div className="ml-2 -mt-1">
<div className="rounded-lg border border-emerald-500/25 bg-emerald-500/5 p-2.5">
<p className="mb-1 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide text-emerald-300">
<ClipboardCheck className="h-3 w-3" /> Ergebnis des Workers
</p>
{fehler ? (
<p className="text-[10px] text-red-400">Ergebnis nicht ladbar</p>
) : erg === null ? (
<p className="text-[10px] text-muted-foreground animate-pulse">Wird geladen </p>
) : text ? (
<p className="max-h-64 overflow-y-auto whitespace-pre-wrap text-xs leading-relaxed text-foreground/85">{text}</p>
) : (
<p className="text-[10px] text-muted-foreground italic">
Der Worker hat keine Abschluss-Zusammenfassung hinterlassen (ältere Karte oder Protokollverstoß).
</p>
)}
</div>
</div>
)
}
function StateBadge({ item }: { item: AuftragItem }) {
const st = item.status
if (!st) return null
+16 -3
View File
@@ -53,11 +53,12 @@ export function EigenlebenView() {
<div className="space-y-7">
<div>
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
Von allein
Skills
</h1>
<p className="text-sm text-muted-foreground">
Was deine Box selbst geleistet hat: ihre Fähigkeiten (Skills) und was aus ihren Vorschlägen wurde.
Das ist derselbe Skill-Satz, mit dem Lucy spricht und die Nacht-Worker arbeiten.
Die Fähigkeiten deiner Box mit Herkunft, Nutzung und ob der Agent sie gerade laden kann
(aktiv/inaktiv). Das ist derselbe Skill-Satz, mit dem Lucy spricht und die Nacht-Worker
arbeiten. Darunter: was aus den Vorschlägen der Box wurde.
</p>
</div>
@@ -147,6 +148,18 @@ function SkillKarte({ skill }: { skill: EigenlebenSkill }) {
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-foreground">{skill.name}</span>
{skill.aktiv === true && (
<span className="flex items-center gap-1 rounded bg-emerald-500/10 px-1.5 py-0.5 text-[9px] font-bold text-emerald-300"
title="Der Agent kann diesen Skill laden und nutzen.">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" /> aktiv
</span>
)}
{skill.aktiv === false && (
<span className="flex items-center gap-1 rounded bg-zinc-500/10 px-1.5 py-0.5 text-[9px] font-bold text-zinc-400"
title="Der Agent kann diesen Skill gerade NICHT laden — Plattform passt nicht (z. B. Apple-Skills auf Linux), Voraussetzungen fehlen oder per Config abgeschaltet.">
<span className="h-1.5 w-1.5 rounded-full bg-zinc-500" /> inaktiv
</span>
)}
{skill.kategorie && (
<span className="rounded bg-background/60 px-1.5 py-0.5 text-[9px] font-semibold text-muted-foreground/80">{skill.kategorie}</span>
)}