feat(chat/activity): Self-improvement event badges for memory & skill updates (#744)

* feat(chat/activity): Self-improvement event badges for memory & skill updates (#737)

* feat(chat): Handle background review.summary WS events for self-improvement notices

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-29 21:43:08 +03:00
1 parent 2cf41ead06
commit 1b596746af
12 files changed
+381 -1

No files matched your search

@@ -471,12 +471,70 @@ private fun AssistantBubble(
}
}
@Composable
private fun SelfImprovementReviewCard(
content: String,
modifier: Modifier = Modifier,
) {
val cleanText =
content
.removePrefix("💾")
.replace(Regex("^\\s*Self-improvement review:\\s*", RegexOption.IGNORE_CASE), "")
.trim()
val isSkill =
cleanText.contains("skill", ignoreCase = true) ||
cleanText.contains("SKILL.md", ignoreCase = true)
val icon = if (isSkill) "⚡" else "🧠"
val title =
if (isSkill) {
"Self-Improvement Review • Skill Patched"
} else {
"Self-Improvement Review • Memory Updated"
}
Card(
modifier =
modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
.testTag("self_improvement_review_card"),
shape = RoundedCornerShape(10.dp),
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
),
) {
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = icon, fontSize = 16.sp)
Spacer(Modifier.width(8.dp))
Text(
text = title,
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.primary,
)
}
Spacer(Modifier.height(4.dp))
Text(
text = cleanText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Composable
private fun SystemBubble(
message: ChatMessage,
onRespondApproval: (String) -> Unit = {},
modifier: Modifier = Modifier,
) {
if (message.content.contains("Self-improvement review:", ignoreCase = true)) {
SelfImprovementReviewCard(content = message.content, modifier = modifier)
return
}
Column(
modifier =
modifier
@@ -941,7 +999,13 @@ fun parseToolOutput(
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
val usage = dataSource.get("usage")?.takeIf { !it.isJsonNull }?.asString
val summaryText = "💾 $action${target?.let { " ($it)" } ?: ""}"
val summaryText =
when (action.lowercase()) {
"add" -> "🧠 Memory Saved${target?.let { " ($it)" } ?: ""}"
"replace", "update" -> "🧠 Memory Updated${target?.let { " ($it)" } ?: ""}"
"remove", "delete" -> "🧠 Memory Removed${target?.let { " ($it)" } ?: ""}"
else -> "🧠 Memory $action${target?.let { " ($it)" } ?: ""}"
}
val mainOutput =
when {
errorMsg != null -> "❌ $errorMsg"
@@ -972,6 +1036,48 @@ fun parseToolOutput(
)
}
// ── Skill Manage-specific formatting (Self-Improvement) ──
if (resolvedToolName == "skill_manage") {
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
val name = argsObj?.get("name")?.takeIf { !it.isJsonNull }?.asString
val category = argsObj?.get("category")?.takeIf { !it.isJsonNull }?.asString
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
val success = dataSource.get("success")?.takeIf { !it.isJsonNull }?.asBoolean ?: true
val msg = dataSource.get("message")?.takeIf { !it.isJsonNull }?.asString
val summaryText =
when (action.lowercase()) {
"create" -> "⚡ Skill Created${name?.let { ": $it" } ?: ""}"
"patch" -> "⚡ Skill Patched${name?.let { ": $it" } ?: ""}"
"edit" -> "⚡ Skill Edited${name?.let { ": $it" } ?: ""}"
"delete" -> "⚡ Skill Archived${name?.let { ": $it" } ?: ""}"
"write_file" -> "⚡ Skill File Written${name?.let { ": $it" } ?: ""}"
"remove_file" -> "⚡ Skill File Removed${name?.let { ": $it" } ?: ""}"
else ->
"⚡ Skill ${action.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(java.util.Locale.getDefault()) else it.toString()
}}${name?.let { ": $it" } ?: ""}"
}
val mainOutput =
when {
errorMsg != null -> "❌ $errorMsg"
!success -> "❌ Skill operation failed"
msg != null -> "✅ $msg"
name != null -> "✅ Skill '$name' $action succeeded${category?.let { " [$it]" } ?: ""}"
else -> "✅ Skill $action done"
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
mainOutput = mainOutput,
durationSec = duration,
isRunning = isRunning,
)
}
// ── Cronjob-specific formatting ──
if (resolvedToolName == "cronjob") {
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
@@ -40,6 +40,7 @@ object ChatWsEventReducer {
is WsEvent.ToolProgress -> event.sessionId
is WsEvent.ToolGenerating -> event.sessionId
is WsEvent.SubagentEvent -> event.sessionId
is WsEvent.ReviewSummary -> event.sessionId
else -> null
}
if (eventSessionId != null && currentSessionId != null && eventSessionId != currentSessionId) {
@@ -95,6 +96,8 @@ object ChatWsEventReducer {
is WsEvent.ClarifyRequest -> onClarifyRequest(state, streamingState, event)
is WsEvent.ReviewSummary -> onReviewSummary(state, streamingState, event)
is WsEvent.RpcError -> onRpcError(state, streamingState, event)
is WsEvent.GatewayError -> onGatewayError(state, streamingState, event)
@@ -506,6 +509,25 @@ object ChatWsEventReducer {
),
)
// ── ReviewSummary (Self-improvement background review) ──────────────
private fun onReviewSummary(
state: ChatUiState,
streamingState: StreamingState,
event: WsEvent.ReviewSummary,
): ReducerResult {
if (event.text.isBlank()) return ReducerResult(state = state, streamingState = streamingState)
val systemMessage =
ChatMessage(
role = MessageRole.SYSTEM,
content = event.text,
)
return ReducerResult(
state = state.copy(messages = state.messages + systemMessage),
streamingState = streamingState,
)
}
// ── RpcError ──────────────────────────────────────────────────────
private fun onRpcError(