This commit is contained in:
8 files changed
+339
-383
No files matched your search
@@ -40,6 +40,7 @@ import com.m57.hermescontrol.data.ws.toAny
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import com.m57.hermescontrol.notification.captureTurnBoundary
|
||||
import com.m57.hermescontrol.notification.correlationScopeId
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolViewCache
|
||||
import com.m57.hermescontrol.ui.common.ActionProgressController
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -51,6 +52,9 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -734,6 +738,24 @@ class ChatViewModel(
|
||||
_uiState.value,
|
||||
)
|
||||
|
||||
init {
|
||||
// Parse tool payloads off the main thread before the transcript
|
||||
// composes them (issue #1327); ToolBubble then reads a cache hit.
|
||||
viewModelScope.launch(searchDispatcher) {
|
||||
_uiState
|
||||
.map { it.messages }
|
||||
.distinctUntilChanged()
|
||||
.conflate()
|
||||
.collect { messages ->
|
||||
for (message in messages) {
|
||||
if (message.role == MessageRole.TOOL) {
|
||||
ToolViewCache.prewarm(message.content, message.toolName, message.isToolRunning)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session ID to resume when the WebSocket connects. Set synchronously by
|
||||
* [ChatScreen] via `SideEffect` during composition — before any WS event
|
||||
|
||||
@@ -73,6 +73,7 @@ import com.m57.hermescontrol.ui.chat.components.DiffViewCard
|
||||
import com.m57.hermescontrol.ui.chat.components.FileViewCard
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolView
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolViewBuilder
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolViewCache
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -133,7 +134,7 @@ internal fun ToolBubble(
|
||||
|
||||
val view =
|
||||
remember(message.content, message.toolName, message.isToolRunning) {
|
||||
parseToolOutput(message.content, message.toolName, message.isToolRunning)
|
||||
ToolViewCache.getOrParse(message.content, message.toolName, message.isToolRunning)
|
||||
}
|
||||
val config = ToolSchemaRegistry.getDisplayConfig(message.toolName)
|
||||
|
||||
|
||||
@@ -1,292 +1,19 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.AccountTree
|
||||
import androidx.compose.material.icons.filled.AutoStories
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.ChatBubble
|
||||
import androidx.compose.material.icons.filled.Checklist
|
||||
import androidx.compose.material.icons.filled.Computer
|
||||
import androidx.compose.material.icons.filled.Description
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Forum
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Keyboard
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import androidx.compose.material.icons.filled.Movie
|
||||
import androidx.compose.material.icons.filled.Photo
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Psychology
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material.icons.filled.ThumbUp
|
||||
import androidx.compose.material.icons.filled.TouchApp
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolCatalog
|
||||
|
||||
/**
|
||||
* Display configuration for a Hermes tool — controls the summary line,
|
||||
* icon, and which arg field is shown when the bubble is collapsed.
|
||||
*
|
||||
* Based on the canonical tool schemas defined in the Hermes Agent source
|
||||
* at /opt/hermes-agent/tools/ (each tool has a `*_SCHEMA` dict).
|
||||
*/
|
||||
/** Display configuration for a Hermes tool — its name and header icon. */
|
||||
data class ToolDisplayConfig(
|
||||
val name: String,
|
||||
/** Material icon for this tool type. */
|
||||
val icon: ImageVector = Icons.Filled.Build,
|
||||
)
|
||||
|
||||
/** Maps tool names to their display config. Ordered by expected frequency of use. */
|
||||
/** Icon lookup backed by the single [ToolCatalog] (issue #1326). */
|
||||
object ToolSchemaRegistry {
|
||||
private val knownTools: Map<String, ToolDisplayConfig> =
|
||||
mapOf(
|
||||
"terminal" to
|
||||
ToolDisplayConfig(
|
||||
name = "terminal",
|
||||
icon = Icons.Filled.Terminal,
|
||||
),
|
||||
"read_file" to
|
||||
ToolDisplayConfig(
|
||||
name = "read_file",
|
||||
icon = Icons.Filled.Description,
|
||||
),
|
||||
"write_file" to
|
||||
ToolDisplayConfig(
|
||||
name = "write_file",
|
||||
icon = Icons.Filled.Edit,
|
||||
),
|
||||
"patch" to
|
||||
ToolDisplayConfig(
|
||||
name = "patch",
|
||||
icon = Icons.Filled.Build,
|
||||
),
|
||||
"search_files" to
|
||||
ToolDisplayConfig(
|
||||
name = "search_files",
|
||||
icon = Icons.Filled.Search,
|
||||
),
|
||||
"web_search" to
|
||||
ToolDisplayConfig(
|
||||
name = "web_search",
|
||||
icon = Icons.Filled.Public,
|
||||
),
|
||||
"browser_navigate" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_navigate",
|
||||
icon = Icons.Filled.Language,
|
||||
),
|
||||
"browser_click" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_click",
|
||||
icon = Icons.Filled.TouchApp,
|
||||
),
|
||||
"browser_snapshot" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_snapshot",
|
||||
icon = Icons.Filled.Photo,
|
||||
),
|
||||
"clarify" to
|
||||
ToolDisplayConfig(
|
||||
name = "clarify",
|
||||
icon = Icons.Filled.ChatBubble,
|
||||
),
|
||||
"delegate_task" to
|
||||
ToolDisplayConfig(
|
||||
name = "delegate_task",
|
||||
icon = Icons.Filled.AccountTree,
|
||||
),
|
||||
"execute_code" to
|
||||
ToolDisplayConfig(
|
||||
name = "execute_code",
|
||||
icon = Icons.Filled.PlayArrow,
|
||||
),
|
||||
"todo" to
|
||||
ToolDisplayConfig(
|
||||
name = "todo",
|
||||
icon = Icons.Filled.Checklist,
|
||||
),
|
||||
"todo_list" to
|
||||
ToolDisplayConfig(
|
||||
name = "todo_list",
|
||||
icon = Icons.Filled.Checklist,
|
||||
),
|
||||
"fact_store" to
|
||||
ToolDisplayConfig(
|
||||
name = "fact_store",
|
||||
icon = Icons.Filled.Psychology,
|
||||
),
|
||||
"session_search" to
|
||||
ToolDisplayConfig(
|
||||
name = "session_search",
|
||||
icon = Icons.Filled.Search,
|
||||
),
|
||||
// ── Action-based tools ──
|
||||
"cronjob" to
|
||||
ToolDisplayConfig(
|
||||
name = "cronjob",
|
||||
icon = Icons.Filled.Schedule,
|
||||
),
|
||||
"cronjob_manage" to
|
||||
ToolDisplayConfig(
|
||||
name = "cronjob_manage",
|
||||
icon = Icons.Filled.Schedule,
|
||||
),
|
||||
"memory" to
|
||||
ToolDisplayConfig(
|
||||
name = "memory",
|
||||
icon = Icons.Filled.Memory,
|
||||
),
|
||||
"fact_feedback" to
|
||||
ToolDisplayConfig(
|
||||
name = "fact_feedback",
|
||||
icon = Icons.Filled.ThumbUp,
|
||||
),
|
||||
"process" to
|
||||
ToolDisplayConfig(
|
||||
name = "process",
|
||||
icon = Icons.Filled.Settings,
|
||||
),
|
||||
"process_manage" to
|
||||
ToolDisplayConfig(
|
||||
name = "process_manage",
|
||||
icon = Icons.Filled.Settings,
|
||||
),
|
||||
"skill_manage" to
|
||||
ToolDisplayConfig(
|
||||
name = "skill_manage",
|
||||
icon = Icons.Filled.Build,
|
||||
),
|
||||
// ── Web / Browser tools ──
|
||||
"web_extract" to
|
||||
ToolDisplayConfig(
|
||||
name = "web_extract",
|
||||
icon = Icons.Filled.Language,
|
||||
),
|
||||
"browser_type" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_type",
|
||||
icon = Icons.Filled.Keyboard,
|
||||
),
|
||||
"browser_scroll" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_scroll",
|
||||
icon = Icons.Filled.TouchApp,
|
||||
),
|
||||
"browser_back" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_back",
|
||||
icon = Icons.Filled.Language,
|
||||
),
|
||||
"browser_press" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_press",
|
||||
icon = Icons.Filled.Keyboard,
|
||||
),
|
||||
"browser_get_images" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_get_images",
|
||||
icon = Icons.Filled.Image,
|
||||
),
|
||||
"browser_vision" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_vision",
|
||||
icon = Icons.Filled.Visibility,
|
||||
),
|
||||
"browser_console" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_console",
|
||||
icon = Icons.Filled.Terminal,
|
||||
),
|
||||
"browser_cdp" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_cdp",
|
||||
icon = Icons.Filled.Build,
|
||||
),
|
||||
"browser_dialog" to
|
||||
ToolDisplayConfig(
|
||||
name = "browser_dialog",
|
||||
icon = Icons.Filled.ChatBubble,
|
||||
),
|
||||
// ── Media / Vision tools ──
|
||||
"vision_analyze" to
|
||||
ToolDisplayConfig(
|
||||
name = "vision_analyze",
|
||||
icon = Icons.Filled.Visibility,
|
||||
),
|
||||
"text_to_speech" to
|
||||
ToolDisplayConfig(
|
||||
name = "text_to_speech",
|
||||
icon = Icons.AutoMirrored.Filled.VolumeUp,
|
||||
),
|
||||
"video_generate" to
|
||||
ToolDisplayConfig(
|
||||
name = "video_generate",
|
||||
icon = Icons.Filled.Movie,
|
||||
),
|
||||
"image_generate" to
|
||||
ToolDisplayConfig(
|
||||
name = "image_generate",
|
||||
icon = Icons.Filled.Image,
|
||||
),
|
||||
// ── Social / Messaging tools ──
|
||||
"x_search" to
|
||||
ToolDisplayConfig(
|
||||
name = "x_search",
|
||||
icon = Icons.Filled.Forum,
|
||||
),
|
||||
"send_message" to
|
||||
ToolDisplayConfig(
|
||||
name = "send_message",
|
||||
icon = Icons.AutoMirrored.Filled.Send,
|
||||
),
|
||||
// ── Skills tools ──
|
||||
"skills_list" to
|
||||
ToolDisplayConfig(
|
||||
name = "skills_list",
|
||||
icon = Icons.AutoMirrored.Filled.MenuBook,
|
||||
),
|
||||
"skill_view" to
|
||||
ToolDisplayConfig(
|
||||
name = "skill_view",
|
||||
icon = Icons.Filled.AutoStories,
|
||||
),
|
||||
// ── On-demand tools ──
|
||||
"tool_search" to
|
||||
ToolDisplayConfig(
|
||||
name = "tool_search",
|
||||
icon = Icons.Filled.Search,
|
||||
),
|
||||
"tool_describe" to
|
||||
ToolDisplayConfig(
|
||||
name = "tool_describe",
|
||||
icon = Icons.Filled.AutoStories,
|
||||
),
|
||||
"tool_call" to
|
||||
ToolDisplayConfig(
|
||||
name = "tool_call",
|
||||
icon = Icons.Filled.Build,
|
||||
),
|
||||
// ── Misc ──
|
||||
"read_terminal" to
|
||||
ToolDisplayConfig(
|
||||
name = "read_terminal",
|
||||
icon = Icons.Filled.Computer,
|
||||
),
|
||||
"computer_use" to
|
||||
ToolDisplayConfig(
|
||||
name = "computer_use",
|
||||
icon = Icons.Filled.Computer,
|
||||
),
|
||||
)
|
||||
|
||||
fun getDisplayConfig(toolName: String?): ToolDisplayConfig =
|
||||
toolName?.let { knownTools[it] } ?: ToolDisplayConfig(name = toolName ?: "tool")
|
||||
ToolDisplayConfig(name = toolName ?: "tool", icon = ToolCatalog.definitionFor(toolName).icon)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.m57.hermescontrol.ui.chat.tool
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.AccountTree
|
||||
import androidx.compose.material.icons.filled.AutoStories
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.ChatBubble
|
||||
import androidx.compose.material.icons.filled.Checklist
|
||||
import androidx.compose.material.icons.filled.Computer
|
||||
import androidx.compose.material.icons.filled.Description
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Forum
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Keyboard
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import androidx.compose.material.icons.filled.Movie
|
||||
import androidx.compose.material.icons.filled.Photo
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Psychology
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material.icons.filled.ThumbUp
|
||||
import androidx.compose.material.icons.filled.TouchApp
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserActionRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserClickRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserConsoleRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserImagesRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserNavigateRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserSnapshotRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserTypeRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserVisionRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ComputerUseRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.CronjobRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.DelegateTaskRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.FactStoreRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.FileEditRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ImageGenerateRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.MemoryRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ProcessRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ProjectListRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ProjectMutateRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ReadFileRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ReadTerminalRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SearchFilesRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SessionSearchRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SkillManageRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SkillViewRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SkillsListRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.TerminalRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.TextToSpeechRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.TodoRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ToolDescribeRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ToolSearchRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.VisionAnalyzeRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.WebExtractRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.WebSearchRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.XSearchRenderer
|
||||
|
||||
/** A tool's visual identity (header/status icon) and its [ToolRenderer]. */
|
||||
internal data class ToolDefinition(
|
||||
val icon: ImageVector,
|
||||
val renderer: ToolRenderer,
|
||||
)
|
||||
|
||||
/** All-default renderer: defers everything to the generic fallbacks. */
|
||||
internal object GenericToolRenderer : ToolRenderer
|
||||
|
||||
/**
|
||||
* The single tool catalog (issue #1326): every known tool declares its icon
|
||||
* and renderer together, so the two can no longer drift apart.
|
||||
*
|
||||
* Unknown tools get the Build icon and [GenericToolRenderer]: every hook
|
||||
* returns null, so [ToolViewBuilder] falls back to the humanized tool name
|
||||
* and the [ToolResultSummary] heuristics — never a raw JSON dump.
|
||||
*/
|
||||
internal object ToolCatalog {
|
||||
val FALLBACK = ToolDefinition(Icons.Filled.Build, GenericToolRenderer)
|
||||
|
||||
val entries: Map<String, ToolDefinition> =
|
||||
mapOf(
|
||||
"browser_back" to ToolDefinition(Icons.Filled.Language, BrowserActionRenderer),
|
||||
"browser_cdp" to ToolDefinition(Icons.Filled.Build, GenericToolRenderer),
|
||||
"browser_click" to ToolDefinition(Icons.Filled.TouchApp, BrowserClickRenderer),
|
||||
"browser_console" to ToolDefinition(Icons.Filled.Terminal, BrowserConsoleRenderer),
|
||||
"browser_dialog" to ToolDefinition(Icons.Filled.ChatBubble, GenericToolRenderer),
|
||||
"browser_fill" to ToolDefinition(Icons.Filled.Keyboard, BrowserTypeRenderer),
|
||||
"browser_get_images" to ToolDefinition(Icons.Filled.Image, BrowserImagesRenderer),
|
||||
"browser_navigate" to ToolDefinition(Icons.Filled.Language, BrowserNavigateRenderer),
|
||||
"browser_press" to ToolDefinition(Icons.Filled.Keyboard, BrowserActionRenderer),
|
||||
"browser_scroll" to ToolDefinition(Icons.Filled.TouchApp, BrowserActionRenderer),
|
||||
"browser_snapshot" to ToolDefinition(Icons.Filled.Photo, BrowserSnapshotRenderer),
|
||||
"browser_type" to ToolDefinition(Icons.Filled.Keyboard, BrowserTypeRenderer),
|
||||
"browser_vision" to ToolDefinition(Icons.Filled.Visibility, BrowserVisionRenderer),
|
||||
"clarify" to ToolDefinition(Icons.Filled.ChatBubble, GenericToolRenderer),
|
||||
"computer_use" to ToolDefinition(Icons.Filled.Computer, ComputerUseRenderer),
|
||||
"cronjob" to ToolDefinition(Icons.Filled.Schedule, CronjobRenderer),
|
||||
"cronjob_manage" to ToolDefinition(Icons.Filled.Schedule, CronjobRenderer),
|
||||
"delegate_task" to ToolDefinition(Icons.Filled.AccountTree, DelegateTaskRenderer),
|
||||
"edit_file" to ToolDefinition(Icons.Filled.Edit, FileEditRenderer),
|
||||
"execute_code" to ToolDefinition(Icons.Filled.PlayArrow, TerminalRenderer),
|
||||
"fact_feedback" to ToolDefinition(Icons.Filled.ThumbUp, GenericToolRenderer),
|
||||
"fact_store" to ToolDefinition(Icons.Filled.Psychology, FactStoreRenderer),
|
||||
"image_generate" to ToolDefinition(Icons.Filled.Image, ImageGenerateRenderer),
|
||||
"memory" to ToolDefinition(Icons.Filled.Memory, MemoryRenderer),
|
||||
"patch" to ToolDefinition(Icons.Filled.Build, FileEditRenderer),
|
||||
"process" to ToolDefinition(Icons.Filled.Settings, ProcessRenderer),
|
||||
"process_manage" to ToolDefinition(Icons.Filled.Settings, ProcessRenderer),
|
||||
"project_create" to ToolDefinition(Icons.Filled.AccountTree, ProjectMutateRenderer),
|
||||
"project_list" to ToolDefinition(Icons.Filled.AccountTree, ProjectListRenderer),
|
||||
"project_switch" to ToolDefinition(Icons.Filled.AccountTree, ProjectMutateRenderer),
|
||||
"read_file" to ToolDefinition(Icons.Filled.Description, ReadFileRenderer),
|
||||
"read_terminal" to ToolDefinition(Icons.Filled.Computer, ReadTerminalRenderer),
|
||||
"search_files" to ToolDefinition(Icons.Filled.Search, SearchFilesRenderer),
|
||||
"send_message" to ToolDefinition(Icons.AutoMirrored.Filled.Send, GenericToolRenderer),
|
||||
"session_search" to ToolDefinition(Icons.Filled.Search, SessionSearchRenderer),
|
||||
"skill_manage" to ToolDefinition(Icons.Filled.Build, SkillManageRenderer),
|
||||
"skill_view" to ToolDefinition(Icons.Filled.AutoStories, SkillViewRenderer),
|
||||
"skills_list" to ToolDefinition(Icons.AutoMirrored.Filled.MenuBook, SkillsListRenderer),
|
||||
"terminal" to ToolDefinition(Icons.Filled.Terminal, TerminalRenderer),
|
||||
"text_to_speech" to ToolDefinition(Icons.AutoMirrored.Filled.VolumeUp, TextToSpeechRenderer),
|
||||
"todo" to ToolDefinition(Icons.Filled.Checklist, TodoRenderer),
|
||||
"todo_list" to ToolDefinition(Icons.Filled.Checklist, TodoRenderer),
|
||||
"tool_call" to ToolDefinition(Icons.Filled.Build, GenericToolRenderer),
|
||||
"tool_describe" to ToolDefinition(Icons.Filled.AutoStories, ToolDescribeRenderer),
|
||||
"tool_search" to ToolDefinition(Icons.Filled.Search, ToolSearchRenderer),
|
||||
"video_generate" to ToolDefinition(Icons.Filled.Movie, GenericToolRenderer),
|
||||
"vision_analyze" to ToolDefinition(Icons.Filled.Visibility, VisionAnalyzeRenderer),
|
||||
"web_extract" to ToolDefinition(Icons.Filled.Language, WebExtractRenderer),
|
||||
"web_search" to ToolDefinition(Icons.Filled.Public, WebSearchRenderer),
|
||||
"write_file" to ToolDefinition(Icons.Filled.Edit, FileEditRenderer),
|
||||
"x_search" to ToolDefinition(Icons.Filled.Forum, XSearchRenderer),
|
||||
)
|
||||
|
||||
fun definitionFor(toolName: String?): ToolDefinition = toolName?.let { entries[it] } ?: FALLBACK
|
||||
}
|
||||
@@ -1,109 +1,6 @@
|
||||
package com.m57.hermescontrol.ui.chat.tool
|
||||
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserActionRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserClickRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserConsoleRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserImagesRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserNavigateRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserSnapshotRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserTypeRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserVisionRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ComputerUseRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.CronjobRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.DelegateTaskRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.FactStoreRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.FileEditRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ImageGenerateRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.MemoryRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ProcessRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ProjectListRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ProjectMutateRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ReadFileRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ReadTerminalRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SearchFilesRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SessionSearchRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SkillManageRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SkillViewRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.SkillsListRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.TerminalRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.TextToSpeechRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.TodoRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ToolDescribeRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.ToolSearchRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.VisionAnalyzeRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.WebExtractRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.WebSearchRenderer
|
||||
import com.m57.hermescontrol.ui.chat.tool.render.XSearchRenderer
|
||||
|
||||
/**
|
||||
* Maps tool names to their display strategy.
|
||||
*
|
||||
* Unknown tools get [GenericToolRenderer]: every hook returns null, so
|
||||
* [ToolViewBuilder] falls back to the humanized tool name and the
|
||||
* [ToolResultSummary] heuristics — never a raw JSON dump.
|
||||
*/
|
||||
/** Renderer lookup backed by the single [ToolCatalog]. */
|
||||
internal object ToolRendererRegistry {
|
||||
/** All-default renderer: defers everything to the generic fallbacks. */
|
||||
private object GenericToolRenderer : ToolRenderer
|
||||
|
||||
private val renderers: Map<String, ToolRenderer> =
|
||||
mapOf(
|
||||
// Shell
|
||||
"terminal" to TerminalRenderer,
|
||||
"execute_code" to TerminalRenderer,
|
||||
"read_terminal" to ReadTerminalRenderer,
|
||||
// Delegation
|
||||
"delegate_task" to DelegateTaskRenderer,
|
||||
// Files
|
||||
"read_file" to ReadFileRenderer,
|
||||
"edit_file" to FileEditRenderer,
|
||||
"patch" to FileEditRenderer,
|
||||
"write_file" to FileEditRenderer,
|
||||
"search_files" to SearchFilesRenderer,
|
||||
// Web
|
||||
"web_search" to WebSearchRenderer,
|
||||
"web_extract" to WebExtractRenderer,
|
||||
"x_search" to XSearchRenderer,
|
||||
// Browser
|
||||
"browser_navigate" to BrowserNavigateRenderer,
|
||||
"browser_snapshot" to BrowserSnapshotRenderer,
|
||||
"browser_click" to BrowserClickRenderer,
|
||||
"browser_fill" to BrowserTypeRenderer,
|
||||
"browser_type" to BrowserTypeRenderer,
|
||||
"browser_scroll" to BrowserActionRenderer,
|
||||
"browser_back" to BrowserActionRenderer,
|
||||
"browser_press" to BrowserActionRenderer,
|
||||
"browser_get_images" to BrowserImagesRenderer,
|
||||
"browser_vision" to BrowserVisionRenderer,
|
||||
"browser_console" to BrowserConsoleRenderer,
|
||||
// Memory / facts
|
||||
"memory" to MemoryRenderer,
|
||||
"fact_store" to FactStoreRenderer,
|
||||
// Task management
|
||||
"cronjob" to CronjobRenderer,
|
||||
"cronjob_manage" to CronjobRenderer,
|
||||
"todo" to TodoRenderer,
|
||||
"todo_list" to TodoRenderer,
|
||||
"session_search" to SessionSearchRenderer,
|
||||
"process" to ProcessRenderer,
|
||||
"process_manage" to ProcessRenderer,
|
||||
// Skills / tool discovery
|
||||
"skills_list" to SkillsListRenderer,
|
||||
"skill_view" to SkillViewRenderer,
|
||||
"skill_manage" to SkillManageRenderer,
|
||||
"tool_search" to ToolSearchRenderer,
|
||||
"tool_describe" to ToolDescribeRenderer,
|
||||
// Media
|
||||
"image_generate" to ImageGenerateRenderer,
|
||||
"vision_analyze" to VisionAnalyzeRenderer,
|
||||
"text_to_speech" to TextToSpeechRenderer,
|
||||
// Projects
|
||||
"project_list" to ProjectListRenderer,
|
||||
"project_create" to ProjectMutateRenderer,
|
||||
"project_switch" to ProjectMutateRenderer,
|
||||
// Desktop automation
|
||||
"computer_use" to ComputerUseRenderer,
|
||||
)
|
||||
|
||||
fun rendererFor(toolName: String): ToolRenderer = renderers[toolName] ?: GenericToolRenderer
|
||||
fun rendererFor(toolName: String): ToolRenderer = ToolCatalog.definitionFor(toolName).renderer
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.m57.hermescontrol.ui.chat.tool
|
||||
|
||||
import com.m57.hermescontrol.ui.chat.parseToolOutput
|
||||
|
||||
/**
|
||||
* Bounded memo of parsed tool payloads (issue #1327).
|
||||
*
|
||||
* The ViewModel [prewarm]s tool rows on a background dispatcher whenever the
|
||||
* transcript changes, so [ToolBubble] normally gets a cache hit during
|
||||
* composition instead of parsing JSON on the main thread. A miss (e.g. a live
|
||||
* event that lands before prewarm finishes) parses synchronously exactly like
|
||||
* before, so the rendered result — and its height — never changes.
|
||||
*
|
||||
* Keyed by the payload itself (not its hash), so a hit is always the view the
|
||||
* same inputs would produce.
|
||||
*/
|
||||
internal object ToolViewCache {
|
||||
private const val MAX_ENTRIES = 512
|
||||
|
||||
private data class Key(
|
||||
val content: String,
|
||||
val toolName: String?,
|
||||
val isRunning: Boolean,
|
||||
)
|
||||
|
||||
/** Boxes the result so a null parse (non-JSON content) is cached too. */
|
||||
private class Entry(
|
||||
val view: ToolView?,
|
||||
)
|
||||
|
||||
private val entries =
|
||||
object : LinkedHashMap<Key, Entry>(64, 0.75f, true) {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Key, Entry>?): Boolean = size > MAX_ENTRIES
|
||||
}
|
||||
|
||||
fun getOrParse(
|
||||
content: String,
|
||||
toolName: String?,
|
||||
isRunning: Boolean,
|
||||
): ToolView? {
|
||||
val key = Key(content, toolName, isRunning)
|
||||
synchronized(entries) { entries[key] }?.let { return it.view }
|
||||
val view = parseToolOutput(content, toolName, isRunning)
|
||||
synchronized(entries) { entries[key] = Entry(view) }
|
||||
return view
|
||||
}
|
||||
|
||||
fun prewarm(
|
||||
content: String,
|
||||
toolName: String?,
|
||||
isRunning: Boolean,
|
||||
) {
|
||||
getOrParse(content, toolName, isRunning)
|
||||
}
|
||||
|
||||
internal fun size(): Int = synchronized(entries) { entries.size }
|
||||
|
||||
internal fun clear() {
|
||||
synchronized(entries) { entries.clear() }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user