Show tool and web search activity in the chat UI
Tool calls were invisible until the answer arrived: the only trace was the sources list, and only for web searches. Surface them as they happen instead. Add llm.stream_tool_calls(), an incremental version of resolve_tool_calls() that yields a start and a completion event per tool call and ends with a "resolved" event carrying the previous return value. resolve_tool_calls() is kept as a thin wrapper that drops the progress events. api/chat.py relays those events as a new SSE "tool" event, and wraps the search-summary/knowledge-save step in one of the same shape, since it is a noticeable pause of its own. On the front end, ToolActivityList.vue renders one row per tool above the assistant bubble (label, target, spinner while running, short result when done). The store merges events by id and marks anything still running as interrupted when the stream ends early. Display only - not persisted, so the rows disappear when the session is reloaded, same as sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
/** アシスタント応答の生成中に実行したツール(Web検索・ノート読み書きなど)を明示的に見せるリスト。 */
|
||||
import type { ToolActivity } from '../api/client'
|
||||
|
||||
defineProps<{ tools: ToolActivity[] }>()
|
||||
|
||||
const ICONS: Record<string, string> = {
|
||||
web_search: '🔍',
|
||||
read_note: '📖',
|
||||
write_note: '✍️',
|
||||
read_knowledge: '🧠',
|
||||
save_knowledge: '💾',
|
||||
}
|
||||
|
||||
function icon(tool: ToolActivity): string {
|
||||
return ICONS[tool.name ?? ''] ?? '🛠'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-activity">
|
||||
<div v-for="tool in tools" :key="tool.id" :class="['tool-row', tool.status]">
|
||||
<span class="icon">{{ icon(tool) }}</span>
|
||||
<span class="label">{{ tool.label || tool.name }}</span>
|
||||
<span v-if="tool.detail" class="detail" :title="tool.detail">{{ tool.detail }}</span>
|
||||
<span v-if="tool.status === 'running'" class="spinner" aria-label="実行中" />
|
||||
<span v-else class="result">{{ tool.status === 'error' ? '⚠' : '✓' }} {{ tool.result }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tool-activity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-width: 72%;
|
||||
padding: 6px 10px;
|
||||
border-left: 2px solid #3f6212;
|
||||
background: #1b2114;
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
.tool-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
color: #a3a3a3;
|
||||
}
|
||||
.tool-row.running .label { color: #d9f99d; }
|
||||
.icon { flex: none; }
|
||||
.label { flex: none; color: #bef264; }
|
||||
.detail {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #d4d4d4;
|
||||
background: #262f1a;
|
||||
border-radius: 8px;
|
||||
padding: 1px 7px;
|
||||
}
|
||||
.result { flex: none; color: #86efac; }
|
||||
.tool-row.error .result { color: #f87171; }
|
||||
.spinner {
|
||||
flex: none;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid #3f6212;
|
||||
border-top-color: #bef264;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user