@@ -630,12 +630,23 @@ private fun ToolRawJsonView(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var formatJson by remember { mutableStateOf(true) }
|
var formatJson by remember { mutableStateOf(true) }
|
||||||
val formattedContent = remember(rawContent) { ToolJson.prettyPrintJson(rawContent) }
|
val clampedRaw = remember(rawContent) { ToolJson.clampForDisplay(rawContent) }
|
||||||
val isFormatDifferent = formattedContent != rawContent
|
|
||||||
val displayText = if (formatJson && isFormatDifferent) formattedContent else rawContent
|
// Full pretty text (for copy) + clamped copy (for display); null when formatting changes nothing.
|
||||||
|
val formatted by produceState<Pair<String, String>?>(initialValue = null, key1 = rawContent) {
|
||||||
|
value =
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val full = ToolJson.prettyPrintJson(rawContent)
|
||||||
|
if (full != rawContent) full to ToolJson.clampForDisplay(full) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isFormatDifferent = formatted != null
|
||||||
|
val displayText = formatted?.takeIf { formatJson }?.second ?: clampedRaw
|
||||||
|
val copyText = formatted?.takeIf { formatJson }?.first ?: rawContent
|
||||||
|
|
||||||
val highlighted by produceState(
|
val highlighted by produceState(
|
||||||
initialValue = remember(displayText) { AnnotatedString(displayText) },
|
initialValue = remember(clampedRaw) { AnnotatedString(clampedRaw) },
|
||||||
key1 = displayText,
|
key1 = displayText,
|
||||||
) {
|
) {
|
||||||
value =
|
value =
|
||||||
@@ -645,7 +656,7 @@ private fun ToolRawJsonView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
CodeTerminalCard(
|
CodeTerminalCard(
|
||||||
textToCopy = displayText,
|
textToCopy = copyText,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
testTag = "tool_raw_json",
|
testTag = "tool_raw_json",
|
||||||
title = "JSON",
|
title = "JSON",
|
||||||
|
|||||||
@@ -84,6 +84,23 @@ internal object ToolCounts {
|
|||||||
"sources" to "source",
|
"sources" to "source",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val S_ENDING_NOUN_REGEX = Regex("(xes|zes|ches|shes|sses)$")
|
||||||
|
private val CONSONANT_Y_NOUN_REGEX = Regex("[aeiou]y$", RegexOption.IGNORE_CASE)
|
||||||
|
private val SIBILANT_NOUN_REGEX = Regex("(s|x|z|ch|sh)$", RegexOption.IGNORE_CASE)
|
||||||
|
private val COUNT_OR_TOTAL_KEY_REGEX = Regex("_count$|_total$")
|
||||||
|
private val COUNT_OR_TOTAL_STRIP_REGEX = Regex("_(count|total)$")
|
||||||
|
private val NUM_PREFIX_STRIP_REGEX = Regex("^num_")
|
||||||
|
private val UNIT_COUNT_REGEX =
|
||||||
|
Regex(
|
||||||
|
"""\b(\d+)\s+(results?|items?|files?|matches?|documents?|sources?|searches?|steps?|rows?)\b""",
|
||||||
|
RegexOption.IGNORE_CASE,
|
||||||
|
)
|
||||||
|
private val VERB_COUNT_REGEX =
|
||||||
|
Regex(
|
||||||
|
"""\b(?:did|found|returned|listed|searched|matched|updated|created|deleted|processed)\s+(\d+)\b""",
|
||||||
|
RegexOption.IGNORE_CASE,
|
||||||
|
)
|
||||||
|
|
||||||
private fun countFromUnknown(value: JsonElement?): Int? {
|
private fun countFromUnknown(value: JsonElement?): Int? {
|
||||||
if (value is JsonArray) {
|
if (value is JsonArray) {
|
||||||
return if (value.isNotEmpty()) value.size else null
|
return if (value.isNotEmpty()) value.size else null
|
||||||
@@ -109,7 +126,7 @@ internal object ToolCounts {
|
|||||||
return "${normalized.dropLast(3)}y"
|
return "${normalized.dropLast(3)}y"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Regex("(xes|zes|ches|shes|sses)$").containsMatchIn(normalized) && normalized.length > 3) {
|
if (S_ENDING_NOUN_REGEX.containsMatchIn(normalized) && normalized.length > 3) {
|
||||||
return normalized.dropLast(2)
|
return normalized.dropLast(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,12 +151,12 @@ internal object ToolCounts {
|
|||||||
|
|
||||||
if (noun.endsWith(
|
if (noun.endsWith(
|
||||||
"y",
|
"y",
|
||||||
) && noun.length > 1 && !Regex("[aeiou]y$", RegexOption.IGNORE_CASE).containsMatchIn(noun)
|
) && noun.length > 1 && !CONSONANT_Y_NOUN_REGEX.containsMatchIn(noun)
|
||||||
) {
|
) {
|
||||||
return "${noun.dropLast(1)}ies"
|
return "${noun.dropLast(1)}ies"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Regex("(s|x|z|ch|sh)$", RegexOption.IGNORE_CASE).containsMatchIn(noun)) {
|
if (SIBILANT_NOUN_REGEX.containsMatchIn(noun)) {
|
||||||
return "${noun}es"
|
return "${noun}es"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +188,12 @@ internal object ToolCounts {
|
|||||||
if (key in COUNT_EXCLUDED_KEYS) {
|
if (key in COUNT_EXCLUDED_KEYS) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!Regex("_count$|_total$").containsMatchIn(key)) {
|
if (!COUNT_OR_TOTAL_KEY_REGEX.containsMatchIn(key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val count = countFromUnknown(value) ?: continue
|
val count = countFromUnknown(value) ?: continue
|
||||||
val stripped = key.lowercase().replace(Regex("_(count|total)$"), "").replace(Regex("^num_"), "")
|
val stripped = key.lowercase().replace(COUNT_OR_TOTAL_STRIP_REGEX, "").replace(NUM_PREFIX_STRIP_REGEX, "")
|
||||||
|
|
||||||
return countMetric(count, singularizeNoun(stripped).ifEmpty { fallbackNoun })
|
return countMetric(count, singularizeNoun(stripped).ifEmpty { fallbackNoun })
|
||||||
}
|
}
|
||||||
@@ -194,14 +211,8 @@ internal object ToolCounts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val unitMatch =
|
val unitMatch =
|
||||||
Regex(
|
UNIT_COUNT_REGEX.find(t)
|
||||||
"""\b(\d+)\s+(results?|items?|files?|matches?|documents?|sources?|searches?|steps?|rows?)\b""",
|
?: VERB_COUNT_REGEX.find(t)
|
||||||
RegexOption.IGNORE_CASE,
|
|
||||||
).find(t)
|
|
||||||
?: Regex(
|
|
||||||
"""\b(?:did|found|returned|listed|searched|matched|updated|created|deleted|processed)\s+(\d+)\b""",
|
|
||||||
RegexOption.IGNORE_CASE,
|
|
||||||
).find(t)
|
|
||||||
|
|
||||||
val n = unitMatch?.groupValues?.get(1)?.toIntOrNull() ?: return null
|
val n = unitMatch?.groupValues?.get(1)?.toIntOrNull() ?: return null
|
||||||
val noun = unitMatch.groupValues.getOrNull(2)?.takeIf { it.isNotEmpty() } ?: fallbackNoun
|
val noun = unitMatch.groupValues.getOrNull(2)?.takeIf { it.isNotEmpty() } ?: fallbackNoun
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ object ToolResultSummary {
|
|||||||
private val ERROR_MSG_KEYS = listOf("message", "reason", "detail")
|
private val ERROR_MSG_KEYS = listOf("message", "reason", "detail")
|
||||||
private val NON_ERROR_TEXT = setOf("", "0", "false", "none", "null", "nil", "ok", "success", "n/a", "na")
|
private val NON_ERROR_TEXT = setOf("", "0", "false", "none", "null", "nil", "ok", "success", "n/a", "na")
|
||||||
|
|
||||||
|
private val KEY_SPLIT_REGEX = Regex("[_\\-.]+")
|
||||||
|
private val WHITESPACE_REGEX = Regex("\\s+")
|
||||||
|
private val ERROR_SIGNAL_STATUS_REGEX =
|
||||||
|
Regex(
|
||||||
|
"\\b(error|failed|failure|fatal|exception)\\b",
|
||||||
|
RegexOption.IGNORE_CASE,
|
||||||
|
)
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun normalize(value: String): String = value.lowercase().trim()
|
private fun normalize(value: String): String = value.lowercase().trim()
|
||||||
@@ -58,7 +66,7 @@ object ToolResultSummary {
|
|||||||
|
|
||||||
private fun titleCase(key: String): String =
|
private fun titleCase(key: String): String =
|
||||||
key
|
key
|
||||||
.split(Regex("[_\\-.]+"))
|
.split(KEY_SPLIT_REGEX)
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.joinToString(" ") { capitalize(it) }
|
.joinToString(" ") { capitalize(it) }
|
||||||
|
|
||||||
@@ -72,7 +80,7 @@ object ToolResultSummary {
|
|||||||
value: String,
|
value: String,
|
||||||
max: Int = 180,
|
max: Int = 180,
|
||||||
): String {
|
): String {
|
||||||
val c = value.replace(Regex("\\s+"), " ").trim()
|
val c = value.replace(WHITESPACE_REGEX, " ").trim()
|
||||||
|
|
||||||
return if (c.length > max) "${c.take(max - 1)}…" else c
|
return if (c.length > max) "${c.take(max - 1)}…" else c
|
||||||
}
|
}
|
||||||
@@ -456,10 +464,7 @@ object ToolResultSummary {
|
|||||||
return (
|
return (
|
||||||
(record["success"] as? JsonPrimitive)?.let { !it.isString && it.content == "false" } == true ||
|
(record["success"] as? JsonPrimitive)?.let { !it.isString && it.content == "false" } == true ||
|
||||||
(record["ok"] as? JsonPrimitive)?.let { !it.isString && it.content == "false" } == true ||
|
(record["ok"] as? JsonPrimitive)?.let { !it.isString && it.content == "false" } == true ||
|
||||||
Regex(
|
ERROR_SIGNAL_STATUS_REGEX.containsMatchIn(status) ||
|
||||||
"\\b(error|failed|failure|fatal|exception)\\b",
|
|
||||||
RegexOption.IGNORE_CASE,
|
|
||||||
).containsMatchIn(status) ||
|
|
||||||
ERROR_KEYS.any { k -> hasMeaningfulErrorValue(record[k]) }
|
ERROR_KEYS.any { k -> hasMeaningfulErrorValue(record[k]) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user