9 files changed
+239
-57
No files matched your search
@@ -8,6 +8,7 @@ import androidx.compose.animation.fadeIn
|
|||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -27,6 +28,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Build
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Error
|
import androidx.compose.material.icons.filled.Error
|
||||||
@@ -43,6 +45,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -57,6 +60,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
@@ -65,13 +69,21 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.CodeTerminalBg
|
||||||
|
import com.m57.hermescontrol.theme.CodeTerminalBorder
|
||||||
|
import com.m57.hermescontrol.theme.CodeTerminalMuted
|
||||||
|
import com.m57.hermescontrol.theme.CodeTerminalText
|
||||||
import com.m57.hermescontrol.theme.HermesStatusColors
|
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
import com.m57.hermescontrol.ui.chat.components.DiffViewCard
|
import com.m57.hermescontrol.ui.chat.components.DiffViewCard
|
||||||
|
import com.m57.hermescontrol.ui.chat.components.highlightSyntax
|
||||||
|
import com.m57.hermescontrol.ui.chat.tool.ToolJson
|
||||||
import com.m57.hermescontrol.ui.chat.tool.ToolView
|
import com.m57.hermescontrol.ui.chat.tool.ToolView
|
||||||
import com.m57.hermescontrol.ui.chat.tool.ToolViewBuilder
|
import com.m57.hermescontrol.ui.chat.tool.ToolViewBuilder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composes the collapsed summary lines for a tool card.
|
* Composes the collapsed summary lines for a tool card.
|
||||||
@@ -237,34 +249,7 @@ internal fun ToolBubble(
|
|||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
if (showRawJson) {
|
if (showRawJson) {
|
||||||
// Raw JSON view — selectable + copy button
|
ToolRawJsonView(rawContent = message.content)
|
||||||
Box {
|
|
||||||
SelectionContainer {
|
|
||||||
Text(
|
|
||||||
text = message.content,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(max = 240.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
style =
|
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
|
||||||
color = contentColor.copy(alpha = 0.8f),
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CopyButton(
|
|
||||||
visible = showCopyButton,
|
|
||||||
textToCopy = message.content,
|
|
||||||
onCopy = { showCopyButton = false },
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.offset(x = 8.dp, y = (-8).dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -310,34 +295,8 @@ internal fun ToolBubble(
|
|||||||
.clickable(role = Role.Button) { showRawJson = true },
|
.clickable(role = Role.Button) { showRawJson = true },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Unparseable content — show raw JSON, selectable
|
// Unparseable content — show raw JSON
|
||||||
Box {
|
ToolRawJsonView(rawContent = message.content)
|
||||||
SelectionContainer {
|
|
||||||
Text(
|
|
||||||
text = message.content,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(max = 240.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
style =
|
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
|
||||||
color = contentColor.copy(alpha = 0.8f),
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CopyButton(
|
|
||||||
visible = showCopyButton,
|
|
||||||
textToCopy = message.content,
|
|
||||||
onCopy = { showCopyButton = false },
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.offset(x = 8.dp, y = (-8).dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,6 +609,123 @@ internal fun SecurityRiskChip(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToolRawJsonView(
|
||||||
|
rawContent: String,
|
||||||
|
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 highlighted by produceState(
|
||||||
|
initialValue = remember(displayText) { AnnotatedString(displayText) },
|
||||||
|
key1 = displayText,
|
||||||
|
) {
|
||||||
|
value =
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
highlightSyntax(displayText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val clipboard = LocalClipboard.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var copied by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(copied) {
|
||||||
|
if (copied) {
|
||||||
|
delay(2000)
|
||||||
|
copied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag("tool_raw_json"),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = CodeTerminalBg,
|
||||||
|
border = BorderStroke(1.dp, CodeTerminalBorder),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "JSON",
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = CodeTerminalMuted,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
if (isFormatDifferent) {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
if (formatJson) {
|
||||||
|
stringResource(R.string.chat_tool_compact_json)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.chat_tool_format_json)
|
||||||
|
},
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.testTag("tool_json_format_toggle")
|
||||||
|
.clickable(role = Role.Button) { formatJson = !formatJson }
|
||||||
|
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, displayText)))
|
||||||
|
}
|
||||||
|
copied = true
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (copied) Icons.Filled.Check else Icons.Filled.ContentCopy,
|
||||||
|
contentDescription =
|
||||||
|
if (copied) {
|
||||||
|
stringResource(R.string.content_desc_copied)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.content_desc_copy)
|
||||||
|
},
|
||||||
|
tint = CodeTerminalMuted,
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectionContainer {
|
||||||
|
Text(
|
||||||
|
text = highlighted,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 280.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = CodeTerminalText,
|
||||||
|
softWrap = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CopyButton(
|
private fun CopyButton(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ private val HIGHLIGHT_TOKENS =
|
|||||||
listOf(
|
listOf(
|
||||||
TokenPattern(Regex("""//[^\n]*"""), CodeComment),
|
TokenPattern(Regex("""//[^\n]*"""), CodeComment),
|
||||||
TokenPattern(Regex("""/\*[\s\S]*?\*/"""), CodeComment),
|
TokenPattern(Regex("""/\*[\s\S]*?\*/"""), CodeComment),
|
||||||
|
TokenPattern(Regex(""""[^"\\]*(\\.[^"\\]*)*"(?=\s*:)"""), CodeKeyword),
|
||||||
TokenPattern(Regex(""""[^"\\]*(\\.[^"\\]*)*""""), CodeString),
|
TokenPattern(Regex(""""[^"\\]*(\\.[^"\\]*)*""""), CodeString),
|
||||||
TokenPattern(Regex("""'[^'\\]*(\\.[^'\\]*)*'"""), CodeString),
|
TokenPattern(Regex("""'[^'\\]*(\\.[^'\\]*)*'"""), CodeString),
|
||||||
TokenPattern(Regex("""`[^`\\]*(\\.[^`\\]*)*`"""), CodeString),
|
TokenPattern(Regex("""`[^`\\]*(\\.[^`\\]*)*`"""), CodeString),
|
||||||
@@ -363,7 +364,7 @@ private val HIGHLIGHT_TOKENS =
|
|||||||
),
|
),
|
||||||
CodeKeyword,
|
CodeKeyword,
|
||||||
),
|
),
|
||||||
TokenPattern(Regex("""[{}()\[\];,.]"""), CodePunctuation),
|
TokenPattern(Regex("""[{}()\[\];:.]"""), CodePunctuation),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -314,4 +314,27 @@ internal object ToolJson {
|
|||||||
|
|
||||||
return formatDurationSeconds(seconds)
|
return formatDurationSeconds(seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val PRETTY_JSON =
|
||||||
|
Json {
|
||||||
|
prettyPrint = true
|
||||||
|
prettyPrintIndent = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty-prints a raw JSON string with 2-space indentation.
|
||||||
|
* Returns the original string if [raw] is not valid JSON.
|
||||||
|
*/
|
||||||
|
fun prettyPrintJson(raw: String): String {
|
||||||
|
val trimmed = raw.trim()
|
||||||
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val element = Json.parseToJsonElement(trimmed)
|
||||||
|
PRETTY_JSON.encodeToString(JsonElement.serializer(), element)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
raw
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -164,9 +164,12 @@
|
|||||||
<string name="chat_tool_fallback">أداة</string>
|
<string name="chat_tool_fallback">أداة</string>
|
||||||
<string name="chat_tool_show_raw">عرض ملف JSON الأولي</string>
|
<string name="chat_tool_show_raw">عرض ملف JSON الأولي</string>
|
||||||
<string name="chat_tool_show_parsed">عرض المخرجات المعالجة</string>
|
<string name="chat_tool_show_parsed">عرض المخرجات المعالجة</string>
|
||||||
|
<string name="chat_tool_format_json">تنسيق JSON</string>
|
||||||
|
<string name="chat_tool_compact_json">ضغط JSON</string>
|
||||||
<string name="content_desc_new_chat">محادثة جديدة</string>
|
<string name="content_desc_new_chat">محادثة جديدة</string>
|
||||||
<string name="content_desc_scroll_to_bottom">التمرير إلى الأسفل</string>
|
<string name="content_desc_scroll_to_bottom">التمرير إلى الأسفل</string>
|
||||||
<string name="content_desc_copy">نسخ</string>
|
<string name="content_desc_copy">نسخ</string>
|
||||||
|
<string name="content_desc_copied">تم النسخ</string>
|
||||||
<string name="content_desc_edit">تعديل</string>
|
<string name="content_desc_edit">تعديل</string>
|
||||||
<string name="chat_msg_tokens">%1$s رمز</string>
|
<string name="chat_msg_tokens">%1$s رمز</string>
|
||||||
<string name="chat_msg_tps">%1$s رمز/ث</string>
|
<string name="chat_msg_tps">%1$s رمز/ث</string>
|
||||||
|
|||||||
@@ -161,9 +161,12 @@
|
|||||||
<string name="chat_tool_fallback">도구</string>
|
<string name="chat_tool_fallback">도구</string>
|
||||||
<string name="chat_tool_show_raw">Raw JSON 보기</string>
|
<string name="chat_tool_show_raw">Raw JSON 보기</string>
|
||||||
<string name="chat_tool_show_parsed">파싱된 결과 보기</string>
|
<string name="chat_tool_show_parsed">파싱된 결과 보기</string>
|
||||||
|
<string name="chat_tool_format_json">JSON 포맷팅</string>
|
||||||
|
<string name="chat_tool_compact_json">JSON 압축</string>
|
||||||
<string name="content_desc_new_chat">새 채팅</string>
|
<string name="content_desc_new_chat">새 채팅</string>
|
||||||
<string name="content_desc_scroll_to_bottom">맨 아래로</string>
|
<string name="content_desc_scroll_to_bottom">맨 아래로</string>
|
||||||
<string name="content_desc_copy">복사</string>
|
<string name="content_desc_copy">복사</string>
|
||||||
|
<string name="content_desc_copied">복사됨</string>
|
||||||
<string name="content_desc_edit">편집</string>
|
<string name="content_desc_edit">편집</string>
|
||||||
<string name="chat_msg_tokens">%1$s 토큰</string>
|
<string name="chat_msg_tokens">%1$s 토큰</string>
|
||||||
<string name="chat_msg_tps">%1$s t/s</string>
|
<string name="chat_msg_tps">%1$s t/s</string>
|
||||||
|
|||||||
@@ -164,9 +164,12 @@
|
|||||||
<string name="chat_tool_fallback">工具</string>
|
<string name="chat_tool_fallback">工具</string>
|
||||||
<string name="chat_tool_show_raw">显示原始 JSON</string>
|
<string name="chat_tool_show_raw">显示原始 JSON</string>
|
||||||
<string name="chat_tool_show_parsed">显示解析输出</string>
|
<string name="chat_tool_show_parsed">显示解析输出</string>
|
||||||
|
<string name="chat_tool_format_json">格式化 JSON</string>
|
||||||
|
<string name="chat_tool_compact_json">压缩 JSON</string>
|
||||||
<string name="content_desc_new_chat">新建对话</string>
|
<string name="content_desc_new_chat">新建对话</string>
|
||||||
<string name="content_desc_scroll_to_bottom">滚动到底部</string>
|
<string name="content_desc_scroll_to_bottom">滚动到底部</string>
|
||||||
<string name="content_desc_copy">复制</string>
|
<string name="content_desc_copy">复制</string>
|
||||||
|
<string name="content_desc_copied">已复制</string>
|
||||||
<string name="content_desc_edit">编辑</string>
|
<string name="content_desc_edit">编辑</string>
|
||||||
<string name="chat_msg_tokens">%1$s tok</string>
|
<string name="chat_msg_tokens">%1$s tok</string>
|
||||||
<string name="chat_msg_tps">%1$s t/s</string>
|
<string name="chat_msg_tps">%1$s t/s</string>
|
||||||
|
|||||||
@@ -182,9 +182,12 @@
|
|||||||
<string name="chat_tool_fallback">Tool</string>
|
<string name="chat_tool_fallback">Tool</string>
|
||||||
<string name="chat_tool_show_raw">Show Raw JSON</string>
|
<string name="chat_tool_show_raw">Show Raw JSON</string>
|
||||||
<string name="chat_tool_show_parsed">Show Parsed Output</string>
|
<string name="chat_tool_show_parsed">Show Parsed Output</string>
|
||||||
|
<string name="chat_tool_format_json">Format JSON</string>
|
||||||
|
<string name="chat_tool_compact_json">Compact JSON</string>
|
||||||
<string name="content_desc_new_chat">New Chat</string>
|
<string name="content_desc_new_chat">New Chat</string>
|
||||||
<string name="content_desc_scroll_to_bottom">Scroll to bottom</string>
|
<string name="content_desc_scroll_to_bottom">Scroll to bottom</string>
|
||||||
<string name="content_desc_copy">Copy</string>
|
<string name="content_desc_copy">Copy</string>
|
||||||
|
<string name="content_desc_copied">Copied</string>
|
||||||
<string name="content_desc_edit">Edit</string>
|
<string name="content_desc_edit">Edit</string>
|
||||||
<string name="chat_msg_tokens">%1$s tok</string>
|
<string name="chat_msg_tokens">%1$s tok</string>
|
||||||
<string name="chat_msg_tps">%1$s t/s</string>
|
<string name="chat_msg_tps">%1$s t/s</string>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat.components
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.theme.CodeKeyword
|
||||||
|
import com.m57.hermescontrol.theme.CodeNumber
|
||||||
|
import com.m57.hermescontrol.theme.CodePunctuation
|
||||||
|
import com.m57.hermescontrol.theme.CodeString
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SyntaxHighlightingTest {
|
||||||
|
@Test
|
||||||
|
fun `highlightSyntax highlights json keys as keyword and values as string`() {
|
||||||
|
val json = """{"status": "running", "code": 0}"""
|
||||||
|
val result = highlightSyntax(json)
|
||||||
|
|
||||||
|
val styles = result.spanStyles
|
||||||
|
val keySpan = styles.find { it.item.color == CodeKeyword }
|
||||||
|
assertNotNull("JSON key should be highlighted with CodeKeyword", keySpan)
|
||||||
|
|
||||||
|
val stringSpan = styles.find { it.item.color == CodeString }
|
||||||
|
assertNotNull("String value should be highlighted with CodeString", stringSpan)
|
||||||
|
|
||||||
|
val numberSpan = styles.find { it.item.color == CodeNumber }
|
||||||
|
assertNotNull("Number value should be highlighted with CodeNumber", numberSpan)
|
||||||
|
|
||||||
|
val punctSpan = styles.find { it.item.color == CodePunctuation }
|
||||||
|
assertNotNull("Colon/braces should be highlighted with CodePunctuation", punctSpan)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat.tool
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ToolJsonTest {
|
||||||
|
@Test
|
||||||
|
fun `prettyPrintJson formats minified json object line-by-line with indentation`() {
|
||||||
|
val raw = """{"name":"test","count":42,"active":true}"""
|
||||||
|
val formatted = ToolJson.prettyPrintJson(raw)
|
||||||
|
|
||||||
|
assertTrue(formatted.contains("\n"))
|
||||||
|
assertTrue(formatted.contains(" \"name\": \"test\""))
|
||||||
|
assertTrue(formatted.contains(" \"count\": 42"))
|
||||||
|
assertTrue(formatted.contains(" \"active\": true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `prettyPrintJson formats json array with indentation`() {
|
||||||
|
val raw = """[{"id":1},{"id":2}]"""
|
||||||
|
val formatted = ToolJson.prettyPrintJson(raw)
|
||||||
|
|
||||||
|
assertTrue(formatted.contains("\n"))
|
||||||
|
assertTrue(formatted.contains(" {\n \"id\": 1\n }"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `prettyPrintJson returns non-json content unchanged`() {
|
||||||
|
val raw = "Execution failed with exit code 1"
|
||||||
|
val formatted = ToolJson.prettyPrintJson(raw)
|
||||||
|
|
||||||
|
assertEquals(raw, formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `prettyPrintJson returns empty and blank strings unchanged`() {
|
||||||
|
assertEquals("", ToolJson.prettyPrintJson(""))
|
||||||
|
assertEquals(" ", ToolJson.prettyPrintJson(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user