Newer
Older
navi-1 / webclient / src / components / messages / SubagentStep.vue
<template>
  <details
    ref="detailsEl"
    class="subagent-step"
    :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">running</span>
      <i class="ph ph-caret-down tool-chevron"></i>
    </summary>
    <div class="tool-card-body">
      <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>
    </div>
  </details>
</template>

<script setup>
import { ref, watch } from '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)

watch(
  () => props.tool.pending,
  (pending, wasPending) => {
    if (!detailsEl.value) return
    if (pending) {
      detailsEl.value.setAttribute('open', '')
    } else if (wasPending !== undefined) {
      detailsEl.value.removeAttribute('open')
    }
  },
  { immediate: true }
)

function renderArgs(val) {
  return formatCompactJson(val)
}

function renderResult(val) {
  if (typeof val === 'string') {
    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 || '&nbsp;'}</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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
}
</script>