<template>
<details
ref="detailsEl"
class="tool-card"
:class="{
'is-success': !tool.pending && tool.success,
'is-error': !tool.pending && !tool.success
}"
>
<summary>
<span class="tool-status-icon">
<span v-if="tool.pending" class="spinner"></span>
<i v-else-if="tool.success" class="ph ph-check-circle"></i>
<i v-else class="ph ph-x-circle"></i>
</span>
<span class="tool-name">{{ tool.name }}</span>
<span v-if="tool.pending" class="tool-running-time">{{ elapsedLabel }}</span>
<i class="ph ph-caret-down tool-chevron"></i>
</summary>
<div class="tool-card-body">
<div v-if="tool.pending && tool.name !== 'spawn_agent'" class="tool-running-banner">
<span class="spinner"></span>
<span>{{ runningLabel }}</span>
</div>
<details v-if="tool.args" class="tool-args">
<summary>
<span class="tool-section-label">Arguments</span>
<i class="ph ph-caret-down tool-chevron"></i>
</summary>
<div class="tool-section">
<pre class="tool-code" v-html="renderArgs(tool.args)"></pre>
</div>
</details>
<div v-if="tool.result != null" class="tool-section tool-result-section">
<div class="tool-section-label">Result</div>
<div class="tool-result" v-html="renderResult(tool.result)"></div>
</div>
<!-- Live terminal output stream -->
<div v-if="tool.name === 'terminal' && tool.terminalOutput" class="tool-section terminal-output-section">
<div class="tool-section-label">Live output</div>
<pre class="terminal-output">{{ tool.terminalOutput }}</pre>
</div>
<!-- Subagent planning indicator (while subagent is in planning phase) -->
<div v-if="tool.planningLabel != null" class="subagent-planning-indicator">
<span class="planning-label">{{ tool.planningLabel }}</span>
</div>
<!-- Subagent steps nested inside spawn_agent card -->
<div v-if="tool.steps?.length" class="subagent-steps">
<template v-for="(step, i) in tool.steps" :key="i">
<ThinkingCard v-if="step.kind === 'turn_thinking'" :msg="step" />
<details v-else-if="step.kind === 'plan'" class="plan-card" open>
<summary>
<i class="ph ph-map-trifold"></i>
Plan
<i class="ph ph-caret-down plan-chevron"></i>
</summary>
<div class="plan-body" v-html="renderMarkdown(step.text)" />
</details>
<SubagentStep v-else :tool="step" />
</template>
</div>
</div>
</details>
</template>
<script setup>
import { ref, computed, watch, onBeforeUnmount } from 'vue'
import SubagentStep from './SubagentStep.vue'
import ThinkingCard from './ThinkingCard.vue'
import { renderMarkdown } from '@/composables/useMarkdown.js'
function looksLikeMarkdown(str) {
if (typeof str !== 'string') return false
const mdPatterns = [
/#{1,6} /, // headings
/\*\*|__/, // bold
/`[^`]+`/, // inline code
/```/, // code block
/^\s*[-*+]\s/m, // list
/^\s*\d+\.\s/m, // ordered list
/\[[^\]]+\]\([^)]+\)/, // link
/\|.*\|/, // table
/^\s*>\s/m, // blockquote
]
return mdPatterns.some(re => re.test(str))
}
const props = defineProps({ tool: { type: Object, required: true } })
const detailsEl = ref(null)
const now = ref(Date.now())
let timer = null
// Auto-open when tool starts, auto-close when done
watch(
() => props.tool.pending,
(pending, wasPending) => {
if (!detailsEl.value) return
if (pending) {
detailsEl.value.setAttribute('open', '')
} else if (wasPending !== undefined) {
// Tool just completed — collapse
detailsEl.value.removeAttribute('open')
}
},
{ immediate: true }
)
watch(
() => props.tool.pending,
(pending) => {
if (pending) startTimer()
else stopTimer()
},
{ immediate: true }
)
onBeforeUnmount(stopTimer)
const runningLabel = computed(() => {
if (props.tool.name === 'filesystem' && props.tool.args?.action) {
return `Running filesystem ${props.tool.args.action}...`
}
return `Running ${props.tool.name}...`
})
const elapsedLabel = computed(() => {
if (!props.tool.startedAt) return ''
const seconds = Math.max(0, Math.floor((now.value - props.tool.startedAt) / 1000))
return seconds < 1 ? 'starting' : `${seconds}s`
})
function startTimer() {
if (timer) return
now.value = Date.now()
timer = setInterval(() => { now.value = Date.now() }, 1000)
}
function stopTimer() {
if (!timer) return
clearInterval(timer)
timer = null
}
function renderArgs(val) {
return formatCompactJson(val)
}
function renderResult(val) {
if (typeof val === 'string') {
// Try JSON first
try {
const parsed = JSON.parse(val)
return formatCompactJson(parsed)
} catch { /* not JSON */ }
if (looksLikeMarkdown(val)) {
return renderMarkdown(val)
}
return escapeHtml(val)
.split('\n')
.map((line, i) => `<div class="result-line">${line || ' '}</div>`)
.join('')
}
return formatCompactJson(val)
}
function formatCompactJson(val) {
if (typeof val === 'string') {
try { val = JSON.parse(val) } catch { return escapeHtml(val) }
}
if (val === null || typeof val !== 'object') return escapeHtml(String(val))
if (Array.isArray(val)) {
if (val.length === 0) return '<span class="json-empty">[]</span>'
const items = val.map((item, i) =>
`<div class="json-item">
<span class="json-key">${i}</span>
<span class="json-sep">:</span>
<span class="json-value">${formatCompactValue(item)}</span>
</div>`
).join('')
return `<div class="json-array">${items}</div>`
}
const entries = Object.entries(val)
if (entries.length === 0) return '<span class="json-empty">{}</span>'
const items = entries.map(([k, v]) =>
`<div class="json-item">
<span class="json-key">${escapeHtml(k)}</span>
<span class="json-sep">:</span>
<span class="json-value">${formatCompactValue(v)}</span>
</div>`
).join('')
return `<div class="json-object">${items}</div>`
}
function formatCompactValue(v) {
if (v === null) return '<span class="json-null">null</span>'
if (typeof v === 'boolean') return `<span class="json-bool">${v}</span>`
if (typeof v === 'number') return `<span class="json-number">${v}</span>`
if (typeof v === 'string') {
const max = 200
const s = escapeHtml(v)
if (s.length <= max) return `<span class="json-string">${s}</span>`
return `<span class="json-string">${s.slice(0, max)}…</span>`
}
if (Array.isArray(v)) return `<span class="json-array">[${v.length} items]</span>`
return `<span class="json-object">{${Object.keys(v).length} keys}</span>`
}
function escapeHtml(str) {
if (typeof str !== 'string') return String(str ?? '')
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
}
</script>