fix(#1215): show omitted process output count (#1232)

This commit is contained in:
m57
2026-09-23 11:26:45 +03:00
committed by GitHub
parent 62b865b7d1
commit e539d650cf
10 changed files with 57 additions and 0 deletions
@@ -495,6 +495,13 @@ private fun ExpandedToolContent(
} }
} }
view.outputCut?.let { omitted ->
Text(
text = stringResource(R.string.chat_tool_output_omitted, omitted),
style = MaterialTheme.typography.labelSmall.copy(color = statusColors.warning),
)
}
// ── Duration footer ── // ── Duration footer ──
view.durationLabel?.let { view.durationLabel?.let {
Text( Text(
@@ -41,6 +41,8 @@ internal interface ToolRenderer {
*/ */
internal data class ToolViewExtras( internal data class ToolViewExtras(
val detailLabel: String? = null, val detailLabel: String? = null,
/** Process output characters omitted by the backend. */
val outputCut: Long? = null,
val stdout: String? = null, val stdout: String? = null,
val stderr: String? = null, val stderr: String? = null,
val exitCode: Int? = null, val exitCode: Int? = null,
@@ -35,6 +35,8 @@ data class ToolView(
val detail: String = "", val detail: String = "",
/** Optional section label above [detail] (e.g. "Search results"). */ /** Optional section label above [detail] (e.g. "Search results"). */
val detailLabel: String? = null, val detailLabel: String? = null,
/** Process output characters omitted by the backend. */
val outputCut: Long? = null,
/** Compact count, e.g. "3 results". */ /** Compact count, e.g. "3 results". */
val countLabel: String? = null, val countLabel: String? = null,
/** Human duration, e.g. "1.2s". */ /** Human duration, e.g. "1.2s". */
@@ -106,6 +106,7 @@ object ToolViewBuilder {
subtitle = subtitle, subtitle = subtitle,
detail = clampedDetail, detail = clampedDetail,
detailLabel = extras.detailLabel, detailLabel = extras.detailLabel,
outputCut = extras.outputCut,
countLabel = countLabel?.let { (count, noun) -> "$count ${ToolCounts.pluralizeNoun(noun, count)}" }, countLabel = countLabel?.let { (count, noun) -> "$count ${ToolCounts.pluralizeNoun(noun, count)}" },
durationLabel = ToolJson.durationLabel(call.result), durationLabel = ToolJson.durationLabel(call.result),
stdout = clampedStdout, stdout = clampedStdout,
@@ -3,9 +3,12 @@ package com.m57.hermescontrol.ui.chat.tool.render
import com.m57.hermescontrol.ui.chat.tool.ToolCall import com.m57.hermescontrol.ui.chat.tool.ToolCall
import com.m57.hermescontrol.ui.chat.tool.ToolJson import com.m57.hermescontrol.ui.chat.tool.ToolJson
import com.m57.hermescontrol.ui.chat.tool.ToolRenderer import com.m57.hermescontrol.ui.chat.tool.ToolRenderer
import com.m57.hermescontrol.ui.chat.tool.ToolViewExtras
import com.m57.hermescontrol.ui.chat.tool.ToolViewStatus
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.longOrNull
/** `cronjob`: job listings with schedules, or the mutated job's key fields. */ /** `cronjob`: job listings with schedules, or the mutated job's key fields. */
internal object CronjobRenderer : ToolRenderer { internal object CronjobRenderer : ToolRenderer {
@@ -214,6 +217,15 @@ internal object SessionSearchRenderer : ToolRenderer {
/** `process`: background session listings and lifecycle acks. */ /** `process`: background session listings and lifecycle acks. */
internal object ProcessRenderer : ToolRenderer { internal object ProcessRenderer : ToolRenderer {
override fun extras(
call: ToolCall,
status: ToolViewStatus,
): ToolViewExtras {
if (status == ToolViewStatus.RUNNING) return ToolViewExtras.NONE
val count = (call.result?.get("output_cut") as? JsonPrimitive)?.takeUnless { it.isString }?.longOrNull
return ToolViewExtras(outputCut = count?.takeIf { it > 0 })
}
override fun subtitle(call: ToolCall): String { override fun subtitle(call: ToolCall): String {
val action = ToolJson.firstString(call.args, listOf("action")) val action = ToolJson.firstString(call.args, listOf("action"))
val procId = ToolJson.firstString(call.args, listOf("session_id")) val procId = ToolJson.firstString(call.args, listOf("session_id"))
+1
View File
@@ -165,6 +165,7 @@
<string name="chat_input_placeholder_type_message">اكتب رسالة\u2026</string> <string name="chat_input_placeholder_type_message">اكتب رسالة\u2026</string>
<string name="chat_tool_execution_error">خطأ في التنفيذ: %1$s</string> <string name="chat_tool_execution_error">خطأ في التنفيذ: %1$s</string>
<string name="chat_tool_exit_code">رمز الخروج: %1$d</string> <string name="chat_tool_exit_code">رمز الخروج: %1$d</string>
<string name="chat_tool_output_omitted">تم حذف %1$d حرف من مخرجات العملية</string>
<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>
+1
View File
@@ -162,6 +162,7 @@
<string name="chat_input_placeholder_type_message">메시지 입력…</string> <string name="chat_input_placeholder_type_message">메시지 입력…</string>
<string name="chat_tool_execution_error">실행 오류: %1$s</string> <string name="chat_tool_execution_error">실행 오류: %1$s</string>
<string name="chat_tool_exit_code">종료 코드: %1$d</string> <string name="chat_tool_exit_code">종료 코드: %1$d</string>
<string name="chat_tool_output_omitted">프로세스 출력에서 %1$d자 생략됨</string>
<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>
+1
View File
@@ -165,6 +165,7 @@
<string name="chat_input_placeholder_type_message">输入消息…</string> <string name="chat_input_placeholder_type_message">输入消息…</string>
<string name="chat_tool_execution_error">执行错误:%1$s</string> <string name="chat_tool_execution_error">执行错误:%1$s</string>
<string name="chat_tool_exit_code">退出码:%1$d</string> <string name="chat_tool_exit_code">退出码:%1$d</string>
<string name="chat_tool_output_omitted">进程输出省略了 %1$d 个字符</string>
<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>
+1
View File
@@ -196,6 +196,7 @@
<string name="chat_input_placeholder_type_message">Type a message\u2026</string> <string name="chat_input_placeholder_type_message">Type a message\u2026</string>
<string name="chat_tool_execution_error">Execution Error: %1$s</string> <string name="chat_tool_execution_error">Execution Error: %1$s</string>
<string name="chat_tool_exit_code">Exit Code: %1$d</string> <string name="chat_tool_exit_code">Exit Code: %1$d</string>
<string name="chat_tool_output_omitted">%1$d characters omitted from process output</string>
<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>
@@ -308,6 +308,35 @@ class ToolViewBuilderTest {
assertEquals("poll: proc-1", view.subtitle) assertEquals("poll: proc-1", view.subtitle)
assertTrue(view.detail.contains("Status: running")) assertTrue(view.detail.contains("Status: running"))
assertTrue(view.detail.contains("listening on port 8080")) assertTrue(view.detail.contains("listening on port 8080"))
assertEquals(null, view.outputCut)
}
@Test
fun `process output reports backend omitted character count for each action`() {
listOf("poll", "wait", "log", "kill").forEach { action ->
val outputKey = if (action == "poll") "output_preview" else "output"
val view =
build(
"process_manage",
"""{"action":"$action","session_id":"proc-1"}""",
"""{"status":"exited","$outputKey":"partial output","output_cut":42}""",
)
assertTrue(view.detail.contains("partial output"))
assertEquals(42L, view.outputCut)
}
}
@Test
fun `process output without positive numeric omission has no notice`() {
listOf("{}", """{"output_cut":0}""", """{"output_cut":"unknown"}""").forEach { metadata ->
val fields = metadata.removePrefix("{").removeSuffix("}")
val result = """{"output":"complete"${if (fields.isNotEmpty()) ",$fields" else ""}}"""
val view = build("process", """{"action":"wait"}""", result)
assertEquals("complete", view.detail)
assertEquals(null, view.outputCut)
}
} }
@Test @Test