Skip to content

Commit 47f28e2

Browse files
committed
Add support for collapsing tool calls with smart summaries
1 parent ddb0dd5 commit 47f28e2

2 files changed

Lines changed: 109 additions & 16 deletions

File tree

llms/ui/modules/chat/ChatBody.mjs

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,107 @@ export const CompactThreadButton = {
775775
}
776776
}
777777

778+
export const ToolCall = {
779+
template: `
780+
<div v-if="collapsed" @click="collapsed = !collapsed" class="cursor-pointer rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
781+
<!-- Tool Call Header -->
782+
<div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50/30 dark:bg-gray-800 space-x-4">
783+
<div class="flex items-center gap-2">
784+
<svg class="size-3.5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
785+
<span class="font-mono text-xs font-bold text-gray-700 dark:text-gray-300">{{ tool.function.name }}</span>
786+
<span v-if="toolSummary" :title="toolSummary" class="font-mono text-xs text-gray-700 dark:text-gray-300 truncate overflow-hidden xl:max-w-2xl lg:max-w-xl md:max-w-lg sm:max-w-sm max-w-xs">{{ toolSummary }}</span>
787+
</div>
788+
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium whitespace-nowrap">Tool Call</span>
789+
</div>
790+
</div>
791+
<div v-else class="rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
792+
<!-- Tool Call Header -->
793+
<div @click="collapsed = !collapsed" class="cursor-pointer px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50/30 dark:bg-gray-800 space-x-4">
794+
<div class="flex items-center gap-2">
795+
<svg class="size-3.5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
796+
<span class="font-mono text-xs font-bold text-gray-700 dark:text-gray-300">{{ tool.function.name }}</span>
797+
</div>
798+
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium whitespace-nowrap">Tool Call</span>
799+
</div>
800+
801+
<ToolArguments :value="tool.function.arguments" />
802+
803+
<ToolOutput :tool="tool" :output="toolOutput" />
804+
</div>
805+
`,
806+
props: {
807+
thread: {
808+
type: Object,
809+
required: true
810+
},
811+
tool: {
812+
type: Object,
813+
required: true
814+
}
815+
},
816+
setup(props) {
817+
const ctx = inject('ctx')
818+
819+
const collapsed = ref(true)
820+
const toolOutput = computed(() => props.thread?.messages?.find(m => m.role === 'tool' && m.tool_call_id === props.tool.id))
821+
const toolFailed = computed(() => {
822+
const output = toolOutput.value
823+
return output?.content?.includes('Error')
824+
})
825+
const toolArgs = computed(() => ctx.utils.toJsonObject(props.tool.function.arguments))
826+
const toolSummary = computed(() => {
827+
const toolName = props.tool.function.name
828+
const args = toolArgs.value
829+
const output = toolOutput.value
830+
if (toolName == 'run_bash' && args.command) {
831+
return args.command
832+
}
833+
else if (toolName == 'skill' && args.name) {
834+
return args.name
835+
}
836+
else if (args.path) {
837+
if (toolName == 'read_text_file') {
838+
return args.path + ' (' + ctx.fmt.humanifyNumber(output?.content?.length || 0) + ')'
839+
} else if (toolName == 'directory_tree') {
840+
const tree = ctx.utils.toJsonObject(output?.content)
841+
let dirCount = 0
842+
let fileCount = 0
843+
const countItems = (items) => {
844+
if (!items) return
845+
items.forEach(item => {
846+
if (item.type == 'file') {
847+
fileCount++
848+
} else if (item.type == 'directory') {
849+
dirCount++
850+
countItems(item.children)
851+
}
852+
})
853+
}
854+
countItems(tree)
855+
return `${args.path} 📁${dirCount} 📄${fileCount}`
856+
}
857+
return args.path
858+
} else if (toolName == 'open' && args.target) {
859+
return args.target
860+
} else if (toolName == 'computer') {
861+
if (args.action) {
862+
return args.action
863+
}
864+
} else if (toolName.startsWith('run_') && args.code) {
865+
const firstLine = args.code.split('\n')[0]
866+
return firstLine
867+
}
868+
return ''
869+
})
870+
871+
return {
872+
collapsed,
873+
toolSummary,
874+
toolOutput,
875+
toolFailed,
876+
}
877+
}
878+
}
778879
export const ChatBody = {
779880
template: `
780881
<div class="flex flex-col h-full">
@@ -837,7 +938,12 @@ export const ChatBody = {
837938
</div>
838939
839940
<!-- Message bubble -->
840-
<div
941+
<div v-if="message.role === 'assistant' && !message.content?.trim() && message.tool_calls && message.tool_calls.length > 0">
942+
<div v-if="message.tool_calls && message.tool_calls.length > 0" class="mb-3 space-y-4">
943+
<ToolCall v-for="(tool, i) in message.tool_calls" :key="i" :thread="currentThread" :tool="tool" />
944+
</div>
945+
</div>
946+
<div v-else
841947
class="message rounded-lg px-4 py-3 relative group"
842948
:class="message.role === 'user'
843949
? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
@@ -870,21 +976,7 @@ export const ChatBody = {
870976
871977
<!-- Tool Calls & Outputs -->
872978
<div v-if="message.tool_calls && message.tool_calls.length > 0" class="mb-3 space-y-4">
873-
<div v-for="(tool, i) in message.tool_calls" :key="i" class="rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
874-
<!-- Tool Call Header -->
875-
<div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50/30 dark:bg-gray-800 space-x-4">
876-
<div class="flex items-center gap-2">
877-
<svg class="size-3.5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
878-
<span class="font-mono text-xs font-bold text-gray-700 dark:text-gray-300">{{ tool.function.name }}</span>
879-
</div>
880-
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Tool Call</span>
881-
</div>
882-
883-
<ToolArguments :value="tool.function.arguments" />
884-
885-
<ToolOutput :tool="tool" :output="getToolOutput(tool.id)" />
886-
887-
</div>
979+
<ToolCall v-for="(tool, i) in message.tool_calls" :key="i" :thread="currentThread" :tool="tool" />
888980
</div>
889981
890982
<!-- Tool Output (Orphaned) -->

llms/ui/modules/chat/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,7 @@ export default {
11121112
ViewTypes,
11131113
ViewToolTypes,
11141114
TextViewer,
1115+
ToolCall,
11151116
ToolArguments,
11161117
ToolOutput,
11171118
CompactThreadButton,

0 commit comments

Comments
 (0)