This commit is contained in:
9 files changed
+179
-42
No files matched your search
@@ -128,14 +128,16 @@ object EventParser {
|
||||
}
|
||||
|
||||
"tool.progress" -> {
|
||||
val toolId = payload?.get("tool_id") as? String
|
||||
val name = payload?.get("name") as? String
|
||||
val preview = payload?.get("preview") as? String
|
||||
WsEvent.ToolProgress(name, preview, sessionId)
|
||||
WsEvent.ToolProgress(name, preview, sessionId, toolId)
|
||||
}
|
||||
|
||||
"tool.generating" -> {
|
||||
val toolId = payload?.get("tool_id") as? String
|
||||
val name = payload?.get("name") as? String
|
||||
WsEvent.ToolGenerating(name, sessionId)
|
||||
WsEvent.ToolGenerating(name, sessionId, toolId)
|
||||
}
|
||||
|
||||
"subagent.spawn_requested", "subagent.start", "subagent.progress", "subagent.complete" -> {
|
||||
|
||||
@@ -104,23 +104,25 @@ sealed class WsEvent {
|
||||
* Live tool execution progress with optional preview content.
|
||||
*
|
||||
* Events: `tool.progress`
|
||||
* Payload: `{ name?: string, preview?: string }`
|
||||
* Payload: `{ tool_id?: string, name?: string, preview?: string }`
|
||||
*/
|
||||
data class ToolProgress(
|
||||
val name: String? = null,
|
||||
val preview: String? = null,
|
||||
val sessionId: String? = null,
|
||||
val toolId: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
/**
|
||||
* Tool generation active state.
|
||||
*
|
||||
* Events: `tool.generating`
|
||||
* Payload: `{ name?: string }`
|
||||
* Payload: `{ tool_id?: string, name?: string }`
|
||||
*/
|
||||
data class ToolGenerating(
|
||||
val name: String? = null,
|
||||
val sessionId: String? = null,
|
||||
val toolId: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
/**
|
||||
|
||||
@@ -639,12 +639,7 @@ object ChatWsEventReducer {
|
||||
OkHttpProvider.json.encodeToString(it.toJsonElement())
|
||||
} ?: ""
|
||||
val messages = state.messages.toMutableList()
|
||||
val toolIdx =
|
||||
messages.indexOfLast {
|
||||
it.role == MessageRole.TOOL &&
|
||||
it.toolName == event.name &&
|
||||
it.toolStatus == ToolStatus.RUNNING
|
||||
}
|
||||
val toolIdx = findToolIndex(messages, event.name, event.data?.get("tool_id") as? String, ToolStatus.RUNNING)
|
||||
if (toolIdx < 0) return ReducerResult(state = state, streamingState = streamingState)
|
||||
|
||||
val updated =
|
||||
@@ -675,10 +670,10 @@ object ChatWsEventReducer {
|
||||
/**
|
||||
* Attaches [ToolOutputRiskData] to the matching tool message.
|
||||
*
|
||||
* Finds the last RUNNING tool message with matching [WsEvent.ToolOutputRisk.name].
|
||||
* If no RUNNING message exists (tool already completed), falls back to the last
|
||||
* COMPLETED tool with that name. This covers the case where the risk event arrives
|
||||
* after tool.complete.
|
||||
* Finds the matching stable tool ID first. If the event has no ID or no ID
|
||||
* match exists, it falls back to the tool name for legacy payloads. It
|
||||
* prefers RUNNING tools and then falls back to COMPLETED tools because the
|
||||
* risk event may arrive after tool.complete.
|
||||
*/
|
||||
private fun onToolOutputRisk(
|
||||
state: ChatUiState,
|
||||
@@ -694,20 +689,11 @@ object ChatWsEventReducer {
|
||||
|
||||
val messages = state.messages.toMutableList()
|
||||
|
||||
// Prefer RUNNING tool, fall back to COMPLETED
|
||||
var toolIdx =
|
||||
messages.indexOfLast {
|
||||
it.role == MessageRole.TOOL &&
|
||||
it.toolName == event.name &&
|
||||
it.toolStatus == ToolStatus.RUNNING
|
||||
}
|
||||
// Prefer RUNNING tool, fall back to COMPLETED. Stable tool IDs win over
|
||||
// name matching so overlapping calls to the same tool stay isolated.
|
||||
var toolIdx = findToolIndex(messages, event.name, event.toolId, ToolStatus.RUNNING)
|
||||
if (toolIdx < 0) {
|
||||
toolIdx =
|
||||
messages.indexOfLast {
|
||||
it.role == MessageRole.TOOL &&
|
||||
it.toolName == event.name &&
|
||||
it.toolStatus == ToolStatus.COMPLETED
|
||||
}
|
||||
toolIdx = findToolIndex(messages, event.name, event.toolId, ToolStatus.COMPLETED)
|
||||
}
|
||||
if (toolIdx < 0) return ReducerResult(state = state, streamingState = streamingState)
|
||||
|
||||
@@ -904,12 +890,7 @@ object ChatWsEventReducer {
|
||||
event: WsEvent.ToolProgress,
|
||||
): ReducerResult {
|
||||
val messages = state.messages.toMutableList()
|
||||
val toolIdx =
|
||||
messages.indexOfLast {
|
||||
it.role == MessageRole.TOOL &&
|
||||
it.toolName == event.name &&
|
||||
it.toolStatus == ToolStatus.RUNNING
|
||||
}
|
||||
val toolIdx = findToolIndex(messages, event.name, event.toolId, ToolStatus.RUNNING)
|
||||
if (toolIdx < 0) return ReducerResult(state = state, streamingState = streamingState)
|
||||
|
||||
messages[toolIdx] = messages[toolIdx].copy(progressPreview = event.preview ?: "")
|
||||
@@ -925,12 +906,7 @@ object ChatWsEventReducer {
|
||||
event: WsEvent.ToolGenerating,
|
||||
): ReducerResult {
|
||||
val messages = state.messages.toMutableList()
|
||||
val toolIdx =
|
||||
messages.indexOfLast {
|
||||
it.role == MessageRole.TOOL &&
|
||||
it.toolName == event.name &&
|
||||
it.toolStatus == ToolStatus.RUNNING
|
||||
}
|
||||
val toolIdx = findToolIndex(messages, event.name, event.toolId, ToolStatus.RUNNING)
|
||||
if (toolIdx < 0) return ReducerResult(state = state, streamingState = streamingState)
|
||||
|
||||
messages[toolIdx] = messages[toolIdx].copy(progressPreview = "")
|
||||
@@ -940,6 +916,30 @@ object ChatWsEventReducer {
|
||||
)
|
||||
}
|
||||
|
||||
/** Match stable tool IDs first, with name matching for legacy events. */
|
||||
private fun findToolIndex(
|
||||
messages: List<ChatMessage>,
|
||||
name: String?,
|
||||
toolId: String?,
|
||||
status: ToolStatus,
|
||||
): Int {
|
||||
if (!toolId.isNullOrBlank()) {
|
||||
val byId =
|
||||
messages.indexOfLast {
|
||||
it.role == MessageRole.TOOL &&
|
||||
it.toolCallId == toolId &&
|
||||
it.toolStatus == status
|
||||
}
|
||||
if (byId >= 0) return byId
|
||||
}
|
||||
|
||||
return messages.indexOfLast {
|
||||
it.role == MessageRole.TOOL &&
|
||||
it.toolName == name &&
|
||||
it.toolStatus == status
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSubagentEvent(
|
||||
state: ChatUiState,
|
||||
streamingState: StreamingState,
|
||||
|
||||
@@ -25,6 +25,9 @@ internal interface ToolRenderer {
|
||||
/** Expanded body text. */
|
||||
fun detail(call: ToolCall): String? = null
|
||||
|
||||
/** Expanded body text while the call is still running. */
|
||||
fun pendingDetail(call: ToolCall): String? = null
|
||||
|
||||
/** Structured extras (streams, diffs, search hits) for special row layouts. */
|
||||
fun extras(
|
||||
call: ToolCall,
|
||||
|
||||
@@ -64,7 +64,13 @@ object ToolViewBuilder {
|
||||
|
||||
val detailBody =
|
||||
ToolJson.stripDividerLines(
|
||||
renderer.detail(call) ?: ToolJson.fallbackDetailText(call.rawArgs, call.rawResult),
|
||||
if (running) {
|
||||
renderer.pendingDetail(call)
|
||||
?: renderer.detail(call)
|
||||
?: ToolJson.fallbackDetailText(call.rawArgs, call.rawResult)
|
||||
} else {
|
||||
renderer.detail(call) ?: ToolJson.fallbackDetailText(call.rawArgs, call.rawResult)
|
||||
},
|
||||
)
|
||||
val detail =
|
||||
if (error.isNotEmpty()) {
|
||||
|
||||
@@ -221,6 +221,11 @@ internal object ProcessRenderer : ToolRenderer {
|
||||
return if (procId.isNotEmpty()) "$action: $procId" else action
|
||||
}
|
||||
|
||||
override fun pendingDetail(call: ToolCall): String {
|
||||
val action = ToolJson.firstString(call.args, listOf("action"))
|
||||
return "⏳ ${action.ifEmpty { "Process operation" }} in progress"
|
||||
}
|
||||
|
||||
override fun detail(call: ToolCall): String {
|
||||
val result = call.result
|
||||
val error = ToolJson.firstString(result, listOf("error"))
|
||||
|
||||
Reference in new issue
Block a user