Newer
Older
navi-1 / webclient / src / components / messages / ToolCard.vue
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 30 Apr 4 KB Reduce spawn agent running indicators
<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-emoji">{{ toolIcon }}</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>
      <div v-if="tool.args" class="tool-section">
        <div class="tool-section-label">Arguments</div>
        <pre class="tool-code">{{ formatJson(tool.args) }}</pre>
      </div>
      <div v-if="tool.result != null" class="tool-section">
        <div class="tool-section-label">Result</div>
        <pre class="tool-code">{{ formatResult(tool.result) }}</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'

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 TOOL_ICONS = {
  web_search: '🔍',
  web_fetch: '🌐',
  filesystem: '📁',
  read_file: '📄',
  write_file: '📝',
  code_exec: '⚙️',
  terminal: '💻',
  ssh_exec: '🖥️',
  spawn_agent: '🤖',
  switch_profile: '🔄',
}

const toolIcon = computed(() => TOOL_ICONS[props.tool.name] ?? '🔧')
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 formatJson(val) {
  if (typeof val === 'string') {
    try { return JSON.stringify(JSON.parse(val), null, 2) } catch { return val }
  }
  return JSON.stringify(val, null, 2)
}

function formatResult(val) {
  if (typeof val === 'string') return val
  return JSON.stringify(val, null, 2)
}
</script>