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 { Mail, Send } from "@lucide/vue";
import DefList from "../components/DefList.vue";
import EmptyState from "../components/EmptyState.vue";
import Pane from "../components/Pane.vue";
import { postAction } from "../api";
import { useDevToolsStore } from "../stores/devtools";

const store = useDevToolsStore();
const to = ref("dev@localhost");
const subject = ref("DevTools test");
const body = ref("Hello from DevTools");
const loading = ref(false);
const error = ref<string | null>(null);

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

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

async function send() {
  loading.value = true;
  error.value = null;
  try {
    const res = await postAction(store.api, "mail", {
      op: "send",
      to: to.value,
      subject: subject.value,
      body: body.value,
    });
    if (!res.ok) error.value = res.error ?? "send failed";
  } 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="hasMail" title="Compose (fake)" :icon="Mail">
      <div class="flex flex-col gap-2">
        <input
          v-model="to"
          type="text"
          placeholder="to"
          class="rounded border border-[var(--dt-border)] bg-[var(--dt-bg)] px-2 py-1.5 text-[11px]"
        />
        <input
          v-model="subject"
          type="text"
          placeholder="subject"
          class="rounded border border-[var(--dt-border)] bg-[var(--dt-bg)] px-2 py-1.5 text-[11px]"
        />
        <textarea
          v-model="body"
          rows="3"
          class="rounded border border-[var(--dt-border)] bg-[var(--dt-bg)] p-2 text-[11px]"
        />
        <button
          type="button"
          class="inline-flex items-center gap-1 self-start rounded bg-[var(--dt-accent)] px-3 py-1.5 text-[11px] font-semibold text-white disabled:opacity-50"
          :disabled="loading"
          @click="send"
        >
          <Send class="size-3.5" /> Send
        </button>
        <p v-if="error" class="text-[11px] text-[var(--dt-err)]">{{ error }}</p>
      </div>
    </Pane>

    <EmptyState
      v-if="!store.current"
      title="No snapshot"
      hint="Select a request first."
      :icon="Mail"
    />
    <EmptyState
      v-else-if="!items.length"
      title="No mail recorded"
      hint="Fake/SMTP sends on this request show up here."
      :icon="Mail"
    />
    <div v-else class="flex flex-col gap-3">
      <Pane
        v-for="(m, i) in items"
        :key="i"
        :title="m.subject || '(no subject)'"
        :icon="Mail"
        :hint="m.backend"
      >
        <DefList
          :items="[
            { label: 'to', value: (m.to || []).join(', ') || '—' },
            { label: 'backend', value: m.backend },
            { label: 'subject', value: m.subject, mono: false },
          ]"
        />
      </Pane>
    </div>
  </div>
</template>