feat(app+brain): App-Bugfixes + Skill-Mgmt-Tools + Voice-Speed persistent + Skill-Browser

App-Bugs:
- Trigger-Liste war leer: brainApi.listTriggers() cast'te {triggers: [...]}
  direkt als Array, t.sort() warf — TriggerBrowser blieb leer. Fix: unwrap.
- GPS-Tracking startete erst bei SettingsScreen-Mount, nicht beim App-Boot.
  Wenn Stefan direkt in den Chat ging, blieb GPS aus. Fix: restoreFromStorage()
  in App.tsx useEffect.
- Text in Chat-Bubbles nicht markierbar / kein Copy-Mechanismus: Bubble jetzt
  Pressable mit onLongPress + neues ⎘-Icon in Status-Row → openBubbleActions().
  Alert-Menu mit "Ganzen Text teilen" + pro extrahierte URL/Mail/Tel eine
  eigene Option. Share.share() — keine neuen Native-Deps noetig.

Brain — Skill-Mgmt:
- ARIA legte beim Skill-Umbau neue Versionen mit Suffix an (Skill-Friedhof),
  weil sie kein Update/Delete-Tool kannte. Zwei neue META_TOOLS in agent.py:
  skill_update (kann entry_code, readme, pip_packages, args, description,
  active patchen — venv wird bei pip_packages-Aenderung rebuilt) + skill_delete.
- skills.py update_skill um entry_code/readme/pip_packages erweitert,
  venv-Rebuild bei pip-Aenderung.

Bridge — Voice-Speed persistent:
- _next_speed_override war pro-Request-Override ohne Persistenz. Bei
  Diagnostic-Chats / Trigger-Replies ohne vorherigen App-Chat fiel der Speed
  auf 1.0 zurueck, ebenso nach Bridge-Restart. Jetzt: _persistent_xtts_speed
  aus voice_config.json (xttsSpeed), wird nach jedem App-chat mit speed
  autopersistiert. TTS-Generation faellt zurueck: per-Request > persistent > 1.0.

App — Feature 6:
- SkillBrowser.tsx: Liste aller Skills, Toggle aktiv/inaktiv, Detail-Modal
  mit Args-Inputs, Ausfuehren mit Live-stdout/stderr, Logs der letzten 20
  Runs, Loeschen. Settings-Sektion "Skills" (🛠️) zwischen Trigger und
  Protokoll. brainApi.listSkills/getSkill/runSkill/updateSkill/deleteSkill/
  getSkillLogs ergaenzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 17:24:03 +02:00
parent 9ed9c99b0e
commit 30c1dd7473
8 changed files with 862 additions and 8 deletions
+101
View File
@@ -123,6 +123,67 @@ META_TOOLS = [
"parameters": {"type": "object", "properties": {}},
},
},
{
"type": "function",
"function": {
"name": "skill_update",
"description": (
"Aktualisiere einen EXISTIERENDEN Skill statt eine zweite Version "
"mit `-v2`/`-new`/`-fixed` Suffix anzulegen. Stefan hasst Skill-"
"Friedhoefe. Wenn Du `youtube2mp3` umbauen sollst → `skill_update` "
"auf den bestehenden, NICHT `skill_create` mit neuem Namen.\n\n"
"Du kannst gleichzeitig `entry_code` (Python-Code austauschen), "
"`readme`, `pip_packages` (bei Aenderung wird die venv automatisch "
"neu aufgebaut), `args`, `description` und `active` setzen. Felder "
"die Du weglaesst bleiben unberuehrt.\n\n"
"WENN Du Dir bei einem grundlegenden API-Bruch unsicher bist ob "
"der Skill noch zum Namen passt: lieber `skill_delete` + "
"`skill_create` mit neuem semantischen Namen statt eines "
"halbgaren Updates."
),
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Bestehender Skill-Name"},
"entry_code": {"type": "string", "description": "Neuer Python-Code (optional)"},
"readme": {"type": "string", "description": "Neuer README-Inhalt (optional)"},
"pip_packages": {
"type": "array",
"items": {"type": "string"},
"description": "Neue pip-Pakete (ueberschreibt komplette Liste; triggert venv-Rebuild)",
},
"args": {
"type": "array",
"items": {"type": "object"},
"description": "Neues Args-Schema (optional)",
},
"description": {"type": "string", "description": "Neue Beschreibung (optional)"},
"active": {"type": "boolean", "description": "Aktivieren/deaktivieren (optional)"},
},
"required": ["name"],
},
},
},
{
"type": "function",
"function": {
"name": "skill_delete",
"description": (
"Loescht einen Skill samt venv und Logs. Nutze das wenn:\n"
"1. Stefan explizit sagt der Skill soll weg\n"
"2. Du eine alte Skill-Version losgeworden bist nachdem `skill_create` "
"mit besserem Namen erfolgreich war (Aufraeumen statt Skill-Friedhof)\n"
"3. Ein Skill grundlegend kaputt und ein Update sich nicht mehr lohnt — "
"in dem Fall bestaetige vorher kurz bei Stefan.\n\n"
"Nicht rueckholbar."
),
"parameters": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
},
},
},
{
"type": "function",
"function": {
@@ -746,6 +807,46 @@ class Agent:
f"- {s['name']} ({s['execution']}) {'aktiv' if s.get('active', True) else 'DEAKTIVIERT'}: {s.get('description', '')}"
for s in items
)
if name == "skill_update":
skill_name = (arguments.get("name") or "").strip()
if not skill_name:
return "FEHLER: name ist Pflicht."
patch: dict = {}
for k in ("entry_code", "readme", "description", "args", "active"):
if k in arguments and arguments[k] is not None:
patch[k] = arguments[k]
if "pip_packages" in arguments and isinstance(arguments["pip_packages"], list):
patch["pip_packages"] = arguments["pip_packages"]
if not patch:
return "FEHLER: keine Felder zum Update angegeben."
try:
manifest = skills_mod.update_skill(skill_name, patch)
except ValueError as exc:
return f"FEHLER: {exc}"
# Side-Channel-Event als skill_created getarnt — gleiche Bubble-Mechanik
# in App/Diagnostic; das Update soll fuer Stefan ebenfalls sichtbar werden.
self._pending_events.append({
"type": "skill_created",
"skill": {
"name": manifest["name"],
"description": manifest.get("description", ""),
"execution": manifest.get("execution", ""),
"active": manifest.get("active", True),
"setup_error": manifest.get("setup_error"),
"updated": True,
},
})
changed = ", ".join(sorted(patch.keys()))
return f"OK — Skill '{skill_name}' aktualisiert ({changed}). active={manifest['active']}"
if name == "skill_delete":
skill_name = (arguments.get("name") or "").strip()
if not skill_name:
return "FEHLER: name ist Pflicht."
try:
skills_mod.delete_skill(skill_name)
except ValueError as exc:
return f"FEHLER: {exc}"
return f"OK — Skill '{skill_name}' geloescht."
if name.startswith("run_"):
skill_name = name[len("run_"):]
res = skills_mod.run_skill(skill_name, args=arguments)