trusty-search 0.2.3

Machine-wide hybrid code search service: BM25 + vector + KG, zero cold-start, MCP server
<script>
  /*
   * Why: The Indexes view is the admin pane for the daemon — list every
   * registered collection, register a new one, kick off a reindex, or evict
   * a stale one. All actions hit the existing flat REST surface.
   * What: Header card with a create form, then a table of every index
   * with per-row Reindex / Delete buttons.
   * Test: Click "Create", enter `demo` + a path, confirm the row appears
   * and Reindex returns a `queued: true` toast.
   */
  import { api } from '../api.js';
  import { getIndexes, getLoading, getError, refreshIndexes } from '../state.svelte.js';

  let indexes = $derived(getIndexes());
  let loading = $derived(getLoading());
  let error = $derived(getError());

  let newId = $state('');
  let newPath = $state('');
  let creating = $state(false);
  let createError = $state(null);
  let rowError = $state(null);
  let busyId = $state(null);

  async function createIndex(e) {
    e?.preventDefault?.();
    if (!newId.trim() || !newPath.trim()) return;
    creating = true;
    createError = null;
    try {
      await api.createIndex(newId.trim(), newPath.trim());
      newId = '';
      newPath = '';
      await refreshIndexes();
    } catch (err) {
      createError = err.message || String(err);
    } finally {
      creating = false;
    }
  }

  async function reindex(id) {
    rowError = null;
    busyId = id;
    try {
      await api.reindex(id);
      await refreshIndexes();
    } catch (err) {
      rowError = `Reindex ${id}: ${err.message || err}`;
    } finally {
      busyId = null;
    }
  }

  async function remove(id) {
    if (!confirm(`Delete index "${id}"? On-disk data is preserved.`)) return;
    rowError = null;
    busyId = id;
    try {
      await api.deleteIndex(id);
      await refreshIndexes();
    } catch (err) {
      rowError = `Delete ${id}: ${err.message || err}`;
    } finally {
      busyId = null;
    }
  }
</script>

<h1 class="page-title">Indexes</h1>

<div class="card mb-4">
  <div class="card-header">Register a new index</div>
  <div class="card-body">
    <form class="create-row" onsubmit={createIndex}>
      <input
        type="text"
        class="input"
        placeholder="Index id (e.g. my-project)"
        bind:value={newId}
      />
      <input
        type="text"
        class="input"
        placeholder="Absolute root path (e.g. /Users/me/code/my-project)"
        bind:value={newPath}
      />
      <button
        type="submit"
        class="btn btn-primary"
        disabled={creating || !newId.trim() || !newPath.trim()}
      >
        {creating ? 'Creating…' : 'Create'}
      </button>
    </form>
    {#if createError}
      <p class="text-sm mt-3" style="color: var(--trusty-danger)">{createError}</p>
    {/if}
  </div>
</div>

<div class="card">
  <div class="card-header flex-between">
    <span>Registered indexes</span>
    <button class="btn btn-sm" onclick={refreshIndexes} disabled={loading}>
      {loading ? 'Refreshing…' : 'Refresh'}
    </button>
  </div>
  <div class="card-body" style="padding: 0">
    {#if rowError}
      <div class="row-error">{rowError}</div>
    {/if}
    {#if error}
      <div class="row-error">{error}</div>
    {/if}
    {#if indexes.length === 0}
      <div class="empty">
        {#if loading}
          Loading…
        {:else}
          No indexes registered yet.
        {/if}
      </div>
    {:else}
      <table class="table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Documents</th>
            <th>Root path</th>
            <th>Status</th>
            <th style="width: 220px; text-align: right">Actions</th>
          </tr>
        </thead>
        <tbody>
          {#each indexes as ix (ix.id)}
            <tr>
              <td><strong>{ix.id}</strong></td>
              <td>{(ix.chunk_count ?? 0).toLocaleString()}</td>
              <td class="text-mono text-xs text-muted truncate" style="max-width: 360px">
                {ix.root_path || '—'}
              </td>
              <td>
                {#if ix.error}
                  <span class="badge badge-danger">error</span>
                {:else}
                  <span class="badge badge-success">ready</span>
                {/if}
              </td>
              <td style="text-align: right">
                <button
                  class="btn btn-sm"
                  disabled={busyId === ix.id}
                  onclick={() => reindex(ix.id)}
                >
                  {busyId === ix.id ? 'Working…' : 'Reindex'}
                </button>
                <button
                  class="btn btn-sm btn-danger"
                  disabled={busyId === ix.id}
                  onclick={() => remove(ix.id)}
                >
                  Delete
                </button>
              </td>
            </tr>
          {/each}
        </tbody>
      </table>
    {/if}
  </div>
</div>

<style>
  .page-title {
    font-size: var(--trusty-fs-xl);
    margin: 0 0 var(--trusty-space-5) 0;
    font-weight: 600;
  }
  .create-row {
    display: grid;
    grid-template-columns: 1fr 2fr auto;
    gap: var(--trusty-space-2);
    align-items: stretch;
  }
  @media (max-width: 720px) {
    .create-row {
      grid-template-columns: 1fr;
    }
  }
  .row-error {
    padding: var(--trusty-space-3) var(--trusty-space-5);
    color: var(--trusty-danger);
    background: var(--trusty-danger-soft);
    border-bottom: 1px solid var(--trusty-border);
    font-size: var(--trusty-fs-sm);
  }
</style>