Newer
Older
navi-1 / webclient / src / components / artifacts / composables / useBackgrounds.js
import { computed, ref, watch } from 'vue'

import { locale, t } from '@/i18n/index.js'

/**
 * The Backgrounds tab: running/finished tasks and terminals in one list, the
 * row that is selected in the detail pane, and the labels/ages it shows.
 */

const TASK_ICONS = {
  terminal: 'ph ph-terminal-window',
  ssh_exec: 'ph ph-plugs-connected',
  spawn_agent: 'ph ph-user-circle-gear',
  code_exec: 'ph ph-code',
  peer: 'ph ph-chats-circle',
}

export function useBackgrounds(chat) {
  // { kind: 'task' | 'terminal', key }
  const selectedBg = ref(null)

  const backgroundsList = computed(() => {
    const tasks = Object.values(chat.backgroundTasks).map(t => ({
      kind: 'task',
      key: t.taskId,
      running: t.status === 'running',
      startedAt: t.startedAt ?? '',
      task: t,
    }))
    const terminals = Object.values(chat.terminals)
      .filter(t => !t.closed || t.output)
      .map(t => ({
        kind: 'terminal',
        key: t.name,
        running: t.status === 'busy' && !t.closed,
        startedAt: '',
        terminal: t,
      }))
    const all = [...tasks, ...terminals]
    return all.sort((a, b) => {
      // running first, then newest started on top
      if (a.running !== b.running) return a.running ? -1 : 1
      return String(b.startedAt ?? '').localeCompare(String(a.startedAt ?? ''))
    })
  })

  const activeBackgroundCount = computed(() =>
    backgroundsList.value.filter(i => i.running).length,
  )

  const selectedTask = computed(() =>
    selectedBg.value?.kind === 'task' ? (chat.backgroundTasks[selectedBg.value.key] || null) : null,
  )
  const selectedTerminal = computed(() =>
    selectedBg.value?.kind === 'terminal' ? (chat.terminals[selectedBg.value.key] || null) : null,
  )

  // A task can finish (TTL, cancel) while selected — drop a dangling selection.
  watch(() => [selectedTask.value, selectedTerminal.value], ([task, term]) => {
    if (selectedBg.value && !task && !term) selectedBg.value = null
  })

  // Selection refers to per-session ids — reset on session switch.
  watch(() => chat.currentId, () => { selectedBg.value = null })

  function selectBackground(item) {
    const sel = { kind: item.kind, key: item.key }
    selectedBg.value = selectedBg.value?.kind === sel.kind && selectedBg.value?.key === sel.key ? null : sel
  }

  function isBgSelected(item) {
    return Boolean(
      selectedBg.value &&
      selectedBg.value.kind === item.kind &&
      selectedBg.value.key === item.key,
    )
  }

  function backgroundIcon(item) {
    return item.kind === 'terminal'
      ? 'ph ph-terminal-window'
      : (TASK_ICONS[item.task.tool] || 'ph ph-arrows-out')
  }

  function backgroundTitle(item) {
    return item.kind === 'terminal'
      ? item.terminal.name
      : (item.task.argsSummary || item.task.tool)
  }

  function backgroundMeta(item) {
    return item.kind === 'terminal'
      ? (item.terminal.description || item.terminal.command || t('artifacts.terminal'))
      : t('artifacts.toolAndAge', { tool: item.task.tool, age: taskAge(item.task) })
  }

  function backgroundBadgeClass(item) {
    if (item.kind === 'terminal') {
      if (item.terminal.closed) return 'badge-error'
      return item.terminal.status === 'busy' ? 'badge-success' : 'badge-secondary'
    }
    return taskBadgeBgClass(item.task)
  }

  function backgroundBadgeLabel(item) {
    if (item.kind === 'terminal') {
      if (item.terminal.closed) return t('artifacts.statusClosed')
      return t(item.terminal.status === 'busy' ? 'artifacts.statusBusy' : 'artifacts.statusIdle')
    }
    return taskBadgeLabel(item.task)
  }

  function taskAge(task) {
    const ts = task.finishedAt ?? task.startedAt
    if (!ts) return ''
    let seconds = (Date.now() - new Date(ts).getTime()) / 1000
    if (!Number.isFinite(seconds) || seconds < 0) return t('common.justNow')
    if (seconds < 60) return `${Math.round(seconds)}s`
    if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
    if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`
    return `${Math.floor(seconds / 86400)}d`
  }

  function taskBadgeClass(task) {
    if (task.status === 'running') return 'status-busy'
    if (task.status === 'completed') return 'status-idle'
    return 'status-closed'  // failed / cancelled
  }

  // Kit badge variant for list badges (filled chip look).
  function taskBadgeBgClass(task) {
    if (task.status === 'running') return 'badge-success'
    if (task.status === 'completed') return 'badge-secondary'
    return 'badge-error'  // failed / cancelled
  }

  function taskBadgeLabel(task) {
    const map = {
      running: t('artifacts.statusRunning'),
      completed: t('artifacts.statusDone'),
      failed: t('artifacts.statusFailed'),
      cancelled: t('artifacts.statusStopped'),
    }
    return map[task.status] || task.status
  }

  function formatTime(value) {
    if (!value) return t('common.notAvailable')
    const date = new Date(value)
    if (Number.isNaN(date.getTime())) return value
    return date.toLocaleTimeString(locale.value, {
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
    })
  }

  function formatUptime(seconds) {
    if (seconds < 60) return `${Math.round(seconds)}s`
    if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`
    const h = Math.floor(seconds / 3600)
    const m = Math.floor((seconds % 3600) / 60)
    return `${h}h ${m}m`
  }

  return {
    selectedBg,
    backgroundsList,
    activeBackgroundCount,
    selectedTask,
    selectedTerminal,
    selectBackground,
    isBgSelected,
    backgroundIcon,
    backgroundTitle,
    backgroundMeta,
    backgroundBadgeClass,
    backgroundBadgeLabel,
    taskBadgeClass,
    taskBadgeLabel,
    formatTime,
    formatUptime,
  }
}