3 files changed
+51
-24
No files matched your search
@@ -630,12 +630,23 @@ private fun ToolRawJsonView(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var formatJson by remember { mutableStateOf(true) }
|
||||
val formattedContent = remember(rawContent) { ToolJson.prettyPrintJson(rawContent) }
|
||||
val isFormatDifferent = formattedContent != rawContent
|
||||
val displayText = if (formatJson && isFormatDifferent) formattedContent else rawContent
|
||||
val clampedRaw = remember(rawContent) { ToolJson.clampForDisplay(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(
|
||||
initialValue = remember(displayText) { AnnotatedString(displayText) },
|
||||
initialValue = remember(clampedRaw) { AnnotatedString(clampedRaw) },
|
||||
key1 = displayText,
|
||||
) {
|
||||
value =
|
||||
@@ -645,7 +656,7 @@ private fun ToolRawJsonView(
|
||||
}
|
||||
|
||||
CodeTerminalCard(
|
||||
textToCopy = displayText,
|
||||
textToCopy = copyText,
|
||||
modifier = modifier,
|
||||
testTag = "tool_raw_json",
|
||||
title = "JSON",
|
||||
|
||||
@@ -84,6 +84,23 @@ internal object ToolCounts {
|
||||
"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? {
|
||||
if (value is JsonArray) {
|
||||
return if (value.isNotEmpty()) value.size else null
|
||||
@@ -109,7 +126,7 @@ internal object ToolCounts {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -134,12 +151,12 @@ internal object ToolCounts {
|
||||
|
||||
if (noun.endsWith(
|
||||
"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"
|
||||
}
|
||||
|
||||
if (Regex("(s|x|z|ch|sh)$", RegexOption.IGNORE_CASE).containsMatchIn(noun)) {
|
||||
if (SIBILANT_NOUN_REGEX.containsMatchIn(noun)) {
|
||||
return "${noun}es"
|
||||
}
|
||||
|
||||
@@ -171,12 +188,12 @@ internal object ToolCounts {
|
||||
if (key in COUNT_EXCLUDED_KEYS) {
|
||||
continue
|
||||
}
|
||||
if (!Regex("_count$|_total$").containsMatchIn(key)) {
|
||||
if (!COUNT_OR_TOTAL_KEY_REGEX.containsMatchIn(key)) {
|
||||
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 })
|
||||
}
|
||||
@@ -194,14 +211,8 @@ internal object ToolCounts {
|
||||
}
|
||||
|
||||
val unitMatch =
|
||||
Regex(
|
||||
"""\b(\d+)\s+(results?|items?|files?|matches?|documents?|sources?|searches?|steps?|rows?)\b""",
|
||||
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)
|
||||
UNIT_COUNT_REGEX.find(t)
|
||||
?: VERB_COUNT_REGEX.find(t)
|
||||
|
||||
val n = unitMatch?.groupValues?.get(1)?.toIntOrNull() ?: return null
|
||||
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 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 ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun normalize(value: String): String = value.lowercase().trim()
|
||||
@@ -58,7 +66,7 @@ object ToolResultSummary {
|
||||
|
||||
private fun titleCase(key: String): String =
|
||||
key
|
||||
.split(Regex("[_\\-.]+"))
|
||||
.split(KEY_SPLIT_REGEX)
|
||||
.filter { it.isNotEmpty() }
|
||||
.joinToString(" ") { capitalize(it) }
|
||||
|
||||
@@ -72,7 +80,7 @@ object ToolResultSummary {
|
||||
value: String,
|
||||
max: Int = 180,
|
||||
): 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
|
||||
}
|
||||
@@ -456,10 +464,7 @@ object ToolResultSummary {
|
||||
return (
|
||||
(record["success"] as? JsonPrimitive)?.let { !it.isString && it.content == "false" } == true ||
|
||||
(record["ok"] as? JsonPrimitive)?.let { !it.isString && it.content == "false" } == true ||
|
||||
Regex(
|
||||
"\\b(error|failed|failure|fatal|exception)\\b",
|
||||
RegexOption.IGNORE_CASE,
|
||||
).containsMatchIn(status) ||
|
||||
ERROR_SIGNAL_STATUS_REGEX.containsMatchIn(status) ||
|
||||
ERROR_KEYS.any { k -> hasMeaningfulErrorValue(record[k]) }
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user