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.data.ws.toJsonElement
|
||||||
import com.m57.hermescontrol.notification.captureTurnBoundary
|
import com.m57.hermescontrol.notification.captureTurnBoundary
|
||||||
import com.m57.hermescontrol.notification.correlationScopeId
|
import com.m57.hermescontrol.notification.correlationScopeId
|
||||||
|
import com.m57.hermescontrol.ui.chat.tool.ToolViewCache
|
||||||
import com.m57.hermescontrol.ui.common.ActionProgressController
|
import com.m57.hermescontrol.ui.common.ActionProgressController
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -51,6 +52,9 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
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.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -734,6 +738,24 @@ class ChatViewModel(
|
|||||||
_uiState.value,
|
_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
|
* Session ID to resume when the WebSocket connects. Set synchronously by
|
||||||
* [ChatScreen] via `SideEffect` during composition — before any WS event
|
* [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.components.FileViewCard
|
||||||
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 com.m57.hermescontrol.ui.chat.tool.ToolViewCache
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ internal fun ToolBubble(
|
|||||||
|
|
||||||
val view =
|
val view =
|
||||||
remember(message.content, message.toolName, message.isToolRunning) {
|
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)
|
val config = ToolSchemaRegistry.getDisplayConfig(message.toolName)
|
||||||
|
|
||||||
|
|||||||
@@ -1,292 +1,19 @@
|
|||||||
package com.m57.hermescontrol.ui.chat
|
package com.m57.hermescontrol.ui.chat
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
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.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 androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import com.m57.hermescontrol.ui.chat.tool.ToolCatalog
|
||||||
|
|
||||||
/**
|
/** Display configuration for a Hermes tool — its name and header icon. */
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
data class ToolDisplayConfig(
|
data class ToolDisplayConfig(
|
||||||
val name: String,
|
val name: String,
|
||||||
/** Material icon for this tool type. */
|
/** Material icon for this tool type. */
|
||||||
val icon: ImageVector = Icons.Filled.Build,
|
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 {
|
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 =
|
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
|
package com.m57.hermescontrol.ui.chat.tool
|
||||||
|
|
||||||
import com.m57.hermescontrol.ui.chat.tool.render.BrowserActionRenderer
|
/** Renderer lookup backed by the single [ToolCatalog]. */
|
||||||
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.
|
|
||||||
*/
|
|
||||||
internal object ToolRendererRegistry {
|
internal object ToolRendererRegistry {
|
||||||
/** All-default renderer: defers everything to the generic fallbacks. */
|
fun rendererFor(toolName: String): ToolRenderer = ToolCatalog.definitionFor(toolName).renderer
|
||||||
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
|
|
||||||
}
|
}
|
||||||
@@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat.tool
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.AccountTree
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Keyboard
|
||||||
|
import androidx.compose.material.icons.filled.Terminal
|
||||||
|
import com.m57.hermescontrol.ui.chat.ToolSchemaRegistry
|
||||||
|
import com.m57.hermescontrol.ui.chat.tool.render.BrowserTypeRenderer
|
||||||
|
import com.m57.hermescontrol.ui.chat.tool.render.FileEditRenderer
|
||||||
|
import com.m57.hermescontrol.ui.chat.tool.render.TerminalRenderer
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ToolCatalogTest {
|
||||||
|
@Test
|
||||||
|
fun `icon and renderer lookups read the same catalog entry`() {
|
||||||
|
for ((name, def) in ToolCatalog.entries) {
|
||||||
|
assertSame(name, def.icon, ToolSchemaRegistry.getDisplayConfig(name).icon)
|
||||||
|
assertSame(name, def.renderer, ToolRendererRegistry.rendererFor(name))
|
||||||
|
assertEquals(name, ToolSchemaRegistry.getDisplayConfig(name).name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `tools that drifted now have a real icon`() {
|
||||||
|
assertSame(Icons.Filled.Edit, ToolSchemaRegistry.getDisplayConfig("edit_file").icon)
|
||||||
|
assertSame(Icons.Filled.Keyboard, ToolSchemaRegistry.getDisplayConfig("browser_fill").icon)
|
||||||
|
for (name in listOf("project_list", "project_create", "project_switch")) {
|
||||||
|
assertSame(name, Icons.Filled.AccountTree, ToolSchemaRegistry.getDisplayConfig(name).icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `known tools keep their renderers and icons`() {
|
||||||
|
assertSame(TerminalRenderer, ToolRendererRegistry.rendererFor("terminal"))
|
||||||
|
assertSame(Icons.Filled.Terminal, ToolSchemaRegistry.getDisplayConfig("terminal").icon)
|
||||||
|
assertSame(FileEditRenderer, ToolRendererRegistry.rendererFor("edit_file"))
|
||||||
|
assertSame(BrowserTypeRenderer, ToolRendererRegistry.rendererFor("browser_fill"))
|
||||||
|
// Icon-only tools still render generically.
|
||||||
|
assertSame(GenericToolRenderer, ToolRendererRegistry.rendererFor("clarify"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unknown tools fall back to build icon and generic renderer`() {
|
||||||
|
assertSame(Icons.Filled.Build, ToolSchemaRegistry.getDisplayConfig("mystery").icon)
|
||||||
|
assertEquals("mystery", ToolSchemaRegistry.getDisplayConfig("mystery").name)
|
||||||
|
assertEquals("tool", ToolSchemaRegistry.getDisplayConfig(null).name)
|
||||||
|
assertSame(GenericToolRenderer, ToolRendererRegistry.rendererFor("mystery"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat.tool
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.ui.chat.parseToolOutput
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotSame
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ToolViewCacheTest {
|
||||||
|
private val payload = """{"name":"terminal","args":{"command":"ls"},"result":{"output":"a","exit_code":0}}"""
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun reset() = ToolViewCache.clear()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cached view equals a fresh parse`() {
|
||||||
|
val cached = ToolViewCache.getOrParse(payload, "terminal", false)
|
||||||
|
assertEquals(parseToolOutput(payload, "terminal", false), cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `prewarm makes composition read a hit`() {
|
||||||
|
ToolViewCache.prewarm(payload, "terminal", false)
|
||||||
|
val first = ToolViewCache.getOrParse(payload, "terminal", false)
|
||||||
|
assertSame(first, ToolViewCache.getOrParse(payload, "terminal", false))
|
||||||
|
assertEquals(1, ToolViewCache.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `running state and content changes are separate entries`() {
|
||||||
|
val running = ToolViewCache.getOrParse(payload, "terminal", true)
|
||||||
|
val done = ToolViewCache.getOrParse(payload, "terminal", false)
|
||||||
|
assertNotSame(running, done)
|
||||||
|
assertEquals(2, ToolViewCache.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `non json content is cached as null`() {
|
||||||
|
assertNull(ToolViewCache.getOrParse("plain text", "terminal", false))
|
||||||
|
assertNull(ToolViewCache.getOrParse("plain text", "terminal", false))
|
||||||
|
assertEquals(1, ToolViewCache.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cache is bounded`() {
|
||||||
|
repeat(600) { i -> ToolViewCache.getOrParse("""{"i":$i}""", "terminal", false) }
|
||||||
|
assertEquals(512, ToolViewCache.size())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user