sova-devtools 0.1.10

In-app DevTools bar for Sova (HTML inject, SSE timeline, request snapshots)
Documentation
<script setup lang="ts">
import { computed, ref } from "vue";
import { HardDrive, Play } from "@lucide/vue";
import DataTable from "../components/DataTable.vue";
import EmptyState from "../components/EmptyState.vue";
import Pane from "../components/Pane.vue";
import { postAction } from "../api";
import { useDevToolsStore } from "../stores/devtools";
import { fmtMs } from "../types";

const store = useDevToolsStore();
const jobName = ref("ping");
const payload = ref("{}");
const loading = ref(false);
const error = ref<string | null>(null);
const lastId = ref<string | null>(null);

const hasTasks = computed(() => {
  const cfg = store.config as { features?: string[] } | null;
  return cfg?.features?.includes("console-tasks") ?? false;
});

const items = computed(() => store.current?.jobs ?? []);

const headers = ["name", "status", "detail", "ms"];
const rows = computed(() =>
  items.value.map((j) => [
    j.name,
    j.status,
    j.detail || "—",
    fmtMs(j.duration_ms),
  ]),
);

async function enqueue() {
  loading.value = true;
  error.value = null;
  let data: unknown = {};
  try {
    data = JSON.parse(payload.value || "{}");
  } catch {
    error.value = "Payload must be valid JSON";
    loading.value = false;
    return;
  }
  try {
    const res = await postAction(store.api, "tasks", {
      name: jobName.value,
      payload: data,
    });
    if (!res.ok) error.value = res.error ?? "enqueue failed";
    else {
      const r = res.result as { id?: string };
      lastId.value = r.id ?? null;
    }
  } catch (e) {
    error.value = e instanceof Error ? e.message : String(e);
  } finally {
    loading.value = false;
  }
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <Pane v-if="hasTasks" title="Enqueue job" :icon="HardDrive">
      <div class="flex flex-wrap items-end gap-2">
        <input
          v-model="jobName"
          type="text"
          placeholder="job name"
          class="w-40 rounded border border-[var(--dt-border)] bg-[var(--dt-bg)] px-2 py-1.5 font-mono text-[11px]"
        />
        <input
          v-model="payload"
          type="text"
          placeholder='{"key":"value"}'
          class="min-w-0 flex-1 rounded border border-[var(--dt-border)] bg-[var(--dt-bg)] px-2 py-1.5 font-mono text-[11px]"
        />
        <button
          type="button"
          class="inline-flex items-center gap-1 rounded bg-[var(--dt-accent)] px-3 py-1.5 text-[11px] font-semibold text-white disabled:opacity-50"
          :disabled="loading"
          @click="enqueue"
        >
          <Play class="size-3.5" /> Enqueue
        </button>
      </div>
      <p v-if="error" class="mt-2 text-[11px] text-[var(--dt-err)]">{{ error }}</p>
      <p v-if="lastId" class="mt-1 font-mono text-[11px] text-[var(--dt-ok)]">
        id: {{ lastId }}
      </p>
    </Pane>

    <EmptyState
      v-if="!store.current"
      title="No snapshot"
      hint="Select a request first."
      :icon="HardDrive"
    />
    <EmptyState
      v-else-if="!items.length"
      title="No jobs on this request"
      hint="Task enqueue / worker events (sova.tasks) appear here."
      :icon="HardDrive"
    />
    <Pane v-else title="Jobs" :icon="HardDrive" :hint="`${items.length}`">
      <DataTable :headers="headers" :rows="rows" :mono-cols="[2]" />
    </Pane>
  </div>
</template>