Refactor parseToolOutput by extracting TODO tool formatting logic (#514)

The parseToolOutput function in ChatBubble.kt was handling the specific
parsing and formatting logic for the "todo" tool inline, making the
function long and harder to read.

This commit extracts the todo-specific formatting logic into a new
private function formatTodoToolOutput, improving the readability and
maintainability of ChatBubble.kt without changing any behavior.
This commit is contained in:
M57 authored and GitHub committed 2026-07-09 18:20:20 +03:00
1 parent e90b69d215
commit bab0c3038d
1 file changed
+67 -54
@@ -532,61 +532,13 @@ data class ParsedToolData(
val isRunning: Boolean = false, val isRunning: Boolean = false,
) )
/** private fun formatTodoToolOutput(
* Parses the tool.complete JSON payload into a [ParsedToolData]. dataSource: JsonObject,
* The gateway sends a payload with `args` and `result` sub-objects args: Map<String, Any?>,
* (see tui_gateway/server.py `_on_tool_complete`). We extract both obj: JsonObject,
* and use the tool name to build a one-line summary from the relevant arg. resolvedToolName: String?,
*/
fun parseToolOutput(
content: String,
toolName: String?,
isRunning: Boolean, isRunning: Boolean,
): ParsedToolData? { ): ParsedToolData {
val trimmed = content.trim()
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null
return try {
val element = OkHttpProvider.json.parseToJsonElement(trimmed)
if (!element.isJsonObject) return null
val obj = element.asJsonObject
// Resolve tool name — from parameter first, then from payload (old sessions)
val resolvedToolName =
toolName
?: obj.get("name")?.takeIf { !it.isJsonNull }?.asString
val config = ToolSchemaRegistry.getDisplayConfig(resolvedToolName)
// Extract args sub-object if present (new tool.complete format)
val argsObj = obj.get("args")?.takeIf { !it.isJsonNull && it.isJsonObject }?.asJsonObject
val args: Map<String, Any?> =
argsObj?.entrySet()?.associate { entry ->
entry.key to (if (entry.value.isJsonPrimitive) entry.value.asString else entry.value.toString())
} ?: emptyMap()
// Extract result sub-object if present, fall back to top-level (old format)
val resultObj = obj.get("result")?.takeIf { !it.isJsonNull && it.isJsonObject }?.asJsonObject
val dataSource = resultObj ?: obj
// Build summary line from args (new format) or context field (old tool.start)
val summaryText =
if (config.summaryArgKey != null) {
val raw = args[config.summaryArgKey]?.toString() ?: ""
val truncated = if (raw.length > 100) raw.take(100) + "…" else raw
if (truncated.isNotBlank()) "${config.summaryPrefix}$truncated" else null
} else {
null ?: obj
.get("context")
?.takeIf { !it.isJsonNull }
?.asString
?.takeIf { it.isNotEmpty() }
?.let { "${config.summaryPrefix}$it" }
}
// ── Todo-specific formatting ──
// Extract the structured todos array and summary object from the result,
// build a clean one-line summary and formatted item list.
if (resolvedToolName == "todo") {
val todosArray = val todosArray =
dataSource dataSource
.get("todos") .get("todos")
@@ -648,6 +600,67 @@ fun parseToolOutput(
durationSec = duration, durationSec = duration,
isRunning = isRunning, isRunning = isRunning,
) )
}
/**
* Parses the tool.complete JSON payload into a [ParsedToolData].
* The gateway sends a payload with `args` and `result` sub-objects
* (see tui_gateway/server.py `_on_tool_complete`). We extract both
* and use the tool name to build a one-line summary from the relevant arg.
*/
fun parseToolOutput(
content: String,
toolName: String?,
isRunning: Boolean,
): ParsedToolData? {
val trimmed = content.trim()
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null
return try {
val element = OkHttpProvider.json.parseToJsonElement(trimmed)
if (!element.isJsonObject) return null
val obj = element.asJsonObject
// Resolve tool name — from parameter first, then from payload (old sessions)
val resolvedToolName =
toolName
?: obj.get("name")?.takeIf { !it.isJsonNull }?.asString
val config = ToolSchemaRegistry.getDisplayConfig(resolvedToolName)
// Extract args sub-object if present (new tool.complete format)
val argsObj = obj.get("args")?.takeIf { !it.isJsonNull && it.isJsonObject }?.asJsonObject
val args: Map<String, Any?> =
argsObj?.entrySet()?.associate { entry ->
entry.key to (if (entry.value.isJsonPrimitive) entry.value.asString else entry.value.toString())
} ?: emptyMap()
// Extract result sub-object if present, fall back to top-level (old format)
val resultObj = obj.get("result")?.takeIf { !it.isJsonNull && it.isJsonObject }?.asJsonObject
val dataSource = resultObj ?: obj
// Build summary line from args (new format) or context field (old tool.start)
val summaryText =
if (config.summaryArgKey != null) {
val raw = args[config.summaryArgKey]?.toString() ?: ""
val truncated = if (raw.length > 100) raw.take(100) + "…" else raw
if (truncated.isNotBlank()) "${config.summaryPrefix}$truncated" else null
} else {
null ?: obj
.get("context")
?.takeIf { !it.isJsonNull }
?.asString
?.takeIf { it.isNotEmpty() }
?.let { "${config.summaryPrefix}$it" }
}
if (resolvedToolName == "todo") {
return formatTodoToolOutput(
dataSource = dataSource,
args = args,
obj = obj,
resolvedToolName = resolvedToolName,
isRunning = isRunning,
)
} }
// ── Fact Store-specific formatting ── // ── Fact Store-specific formatting ──