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
+73 -2
View File
@@ -22,6 +22,8 @@ import {
AppState,
NativeModules,
Alert,
Pressable,
Share,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import RNFS from 'react-native-fs';
@@ -1987,7 +1989,7 @@ const ChatScreen: React.FC = () => {
}
return (
<View
<Pressable
style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble, searchHighlightStyle]}
onLayout={e => {
// Echte Hoehe in Cache speichern — Pre-Scroll der Suche nutzt
@@ -1995,6 +1997,9 @@ const ChatScreen: React.FC = () => {
// unbekannten Items faellt's auf AVG_BUBBLE_HEIGHT zurueck.
itemHeights.current.set(item.id, e.nativeEvent.layout.height);
}}
onLongPress={() => openBubbleActions(item)}
delayLongPress={500}
android_ripple={null}
>
{/* Anhang-Vorschau */}
{item.attachments?.map((att, idx) => (
@@ -2125,6 +2130,15 @@ const ChatScreen: React.FC = () => {
) : null}
<View style={styles.statusRow}>
<Text style={styles.timestamp}>{time}</Text>
{item.text.length > 0 ? (
<TouchableOpacity
hitSlop={{top:6,bottom:6,left:6,right:6}}
onPress={() => openBubbleActions(item)}
accessibilityLabel="Aktionen"
>
<Text style={styles.bubbleCopyIcon}>{'⎘'}</Text>
</TouchableOpacity>
) : null}
{isUser && item.deliveryStatus ? (
item.deliveryStatus === 'failed' && item.clientMsgId ? (
<TouchableOpacity
@@ -2148,7 +2162,58 @@ const ChatScreen: React.FC = () => {
)
) : null}
</View>
</View>
</Pressable>
);
};
// Extrahiert kopierbare Items aus dem Bubble-Text (URLs, Mails, Telefon).
// Wird vom Long-Press/Copy-Menu genutzt damit Stefan den einzelnen Wert
// teilen kann ohne den umliegenden Text mitzunehmen.
const extractCopyables = (text: string): { label: string; value: string }[] => {
const items: { label: string; value: string }[] = [];
const urlRe = /https?:\/\/[^\s<>"']+/gi;
const mailRe = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
const telRe = /(?:\+?\d[\d ()/-]{6,}\d)/g;
const seen = new Set<string>();
const push = (label: string, value: string) => {
const trimmed = value.trim().replace(/[,;.)\]}>]+$/g, '');
if (!trimmed || seen.has(trimmed)) return;
seen.add(trimmed);
items.push({ label, value: trimmed });
};
(text.match(urlRe) || []).forEach(u => push('URL', u));
(text.match(mailRe) || []).forEach(m => push('E-Mail', m));
(text.match(telRe) || []).forEach(t => push('Telefon', t));
return items.slice(0, 5); // max 5 items, mehr wird unleserlich
};
// Long-Press oder ⎘-Icon auf einer Bubble. Zeigt einen Alert mit
// "Text teilen" (= System-Share-Sheet, dort gibt's auch Zwischenablage)
// sowie pro extrahierte URL/E-Mail/Telefonnummer eine Option um
// gezielt nur dieses Item zu teilen.
const openBubbleActions = (item: ChatMessage) => {
const text = showSystemHints ? item.text : stripSystemHints(item.text);
if (!text) return;
const copyables = extractCopyables(text);
const buttons: any[] = [
{
text: '📋 Ganzen Text teilen',
onPress: () => Share.share({ message: text }).catch(() => {}),
},
];
for (const c of copyables) {
buttons.push({
text: `📎 ${c.label}: ${c.value.slice(0, 32)}${c.value.length > 32 ? '…' : ''}`,
onPress: () => Share.share({ message: c.value }).catch(() => {}),
});
}
buttons.push({ text: 'Abbrechen', style: 'cancel' });
Alert.alert(
'Bubble-Aktionen',
copyables.length > 0
? 'Was moechtest du teilen / kopieren?'
: 'Text in System-Share-Sheet oeffnen (dort "In Zwischenablage" verfuegbar).',
buttons,
);
};
@@ -3126,6 +3191,12 @@ const styles = StyleSheet.create({
fontSize: 12,
color: '#FF6B6B',
},
bubbleCopyIcon: {
fontSize: 13,
color: '#8888AA',
marginLeft: 6,
opacity: 0.7,
},
fullscreenOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.95)',