confer-cli 0.8.7

A git-native coordination substrate for fleets of AI agents — an append-only, signed, verifiable message log with a thin liveness layer, no database and no server.
<script lang="ts">
  // Piece 7's repo drill-in (ui/REDESIGN.md, `07-repos-integrity-gravity.html`)
  // — "hot files ranked by conversation density (mini code-ref cards) → the
  // big code view, plus its identity and the actions to keep it mapped."
  // Same overlay convention as TicketFullPopover/NotePopover. Fetches the
  // repo's real reverse-index hits ONCE, on open (reuses the SAME
  // `getRefs(hub, repo, true)` + `groupRefHitsByFile` CodeLens's own repo-
  // rollup already established — design/44 §6 item 2.4 — not a new query
  // shape) — every number here (ref count, distinct messages, topics, hot
  // files) is real, nothing estimated.
  import type { RepoIndexEntry } from '../repoIndex';
  import { api } from '../api';
  import { groupRefHitsByFile, type FileRollupGroup } from '../codeTree';
  import { isTypingTarget } from '../keys';

  interface Props {
    open: boolean;
    entry: RepoIndexEntry | null;
    hub: string;
    /** Jumps to the Code view — a specific file if given, else the whole
     * repo's rollup (same distinction CodeTree's own file-vs-repo-node
     * click already makes). */
    onOpenCode?: (repo: string, path?: string) => void;
    onClose?: () => void;
  }

  let { open, entry, hub, onOpenCode, onClose }: Props = $props();

  const showing = $derived(open && entry !== null);

  const HOT_FILES_VISIBLE = 4;

  let loading = $state(false);
  let hits = $state<import('../types').RefHit[]>([]);
  let loadedFor = $state<string | null>(null);

  async function load(slug: string, hubId: string) {
    loading = true;
    try {
      hits = await api.getRefs(hubId, slug, true);
      loadedFor = slug;
    } catch (err) {
      console.error('confer serve: failed to load repo reverse-index', slug, err);
      hits = [];
      loadedFor = slug;
    } finally {
      loading = false;
    }
  }

  $effect(() => {
    if (!showing || !entry) return;
    if (loadedFor !== entry.slug) void load(entry.slug, hub);
  });

  const hotFiles = $derived<FileRollupGroup[]>(entry && loadedFor === entry.slug ? groupRefHitsByFile(hits) : []);
  const visibleFiles = $derived(hotFiles.slice(0, HOT_FILES_VISIBLE));
  const moreCount = $derived(Math.max(0, hotFiles.length - HOT_FILES_VISIBLE));

  const messageCount = $derived(new Set(hits.map((h) => h.msgId)).size);
  const topicCount = $derived(new Set(hits.map((h) => h.topic).filter((t): t is string => t !== null)).size);

  const maxFileCount = $derived(Math.max(1, ...hotFiles.map((f) => f.count)));

  function handleKeydown(e: KeyboardEvent) {
    if (!showing) return;
    if (isTypingTarget(e.target)) return;
    if (e.key === 'Escape') {
      e.preventDefault();
      onClose?.();
    }
  }
</script>

<svelte:window onkeydown={handleKeydown} />

{#if showing && entry}
  <div class="rd-overlay">
    <div class="rd-backdrop" onclick={onClose} aria-hidden="true" data-testid="repo-detail-backdrop"></div>
    <div class="rd-panel" role="dialog" aria-modal="true" aria-label="Repo detail" tabindex="-1" data-testid="repo-detail-popover">
      <div class="rd-head">
        <span class="slug mono">{entry.slug}</span>
        {#if entry.role}<span class="role">{entry.role}</span>{/if}
        {#if entry.cloned}
          <span class="clone ok mono">✓ cloned · {entry.clonePath}</span>
        {:else}
          <span class="clone mono">{entry.tier === 'shadow' ? '◇ shadow — not registered' : '◑ not cloned here'}</span>
        {/if}
        <button type="button" class="rd-close" aria-label="Close repo detail" onclick={onClose}>esc ✕</button>
      </div>

      <div class="rd-grid">
        <div class="rd-main">
          <span class="lab">hot files <span class="dim">· most referenced in conversation</span></span>
          {#if loading}
            <p class="rd-note">loading…</p>
          {:else if hotFiles.length === 0}
            <p class="rd-note">nothing referenced yet</p>
          {:else}
            {#each visibleFiles as f (f.path)}
              <button type="button" class="filerow" onclick={() => onOpenCode?.(entry!.slug, f.path)} data-testid="repo-hot-file">
                <span class="gutter"><i style="height:{Math.max(3, (f.count / maxFileCount) * 15)}px"></i></span>
                <span class="path mono">{f.path}</span>
                <span class="conv mono"><b>{f.count}</b> ref{f.count === 1 ? '' : 's'}</span>
              </button>
            {/each}
            {#if moreCount > 0}
              <div class="filerow more">
                <span class="gutter"></span>
                <span class="path">+ {moreCount} more referenced file{moreCount === 1 ? '' : 's'}</span>
                <span class="conv"></span>
              </div>
            {/if}
          {/if}
        </div>

        <aside class="rd-side">
          {#if entry.url}
            <div class="kv"><span class="k">remote</span><span class="v mono">{entry.url}</span></div>
          {/if}
          <div class="kv">
            <span class="k">local clone</span>
            <span class="v mono" class:ok={entry.cloned}>{entry.cloned ? `${entry.clonePath} ✓ mapped` : 'none'}</span>
          </div>
          {#if entry.role}<div class="kv"><span class="k">role</span><span class="v mono">{entry.role}</span></div>{/if}
          {#if entry.access.length}
            <div class="kv"><span class="k">access</span><span class="v">{entry.access.join(', ')}</span></div>
          {/if}
          <div class="kv">
            <span class="k">referenced</span>
            <span class="v"
              >{entry.refCount} refs{!loading
                ? ` · across ${messageCount} message${messageCount === 1 ? '' : 's'} · ${topicCount} topic${topicCount === 1 ? '' : 's'}`
                : ''}</span
            >
          </div>
          <div class="rd-actions">
            <button type="button" class="btn" onclick={() => onOpenCode?.(entry!.slug)}>open in code view ›</button>
          </div>
          <!-- DEFERRED: state-mutating actions (map/register/pull) wait on
               a future human-action layer — confer serve is read-only
               today. Kept as intent, not a fake button: dashed, inert,
               titled so it reads as "coming", never a silent no-op. -->
          <div class="rd-deferred">
            <span class="lab" style="display:block;margin-bottom:5px;font-size:9px">later · needs the human-action layer</span>
            {#if entry.tier === 'shadow'}
              <span class="dbtn" title="serve is read-only today — this will register the repo once the human-action layer lands">register</span>
            {:else if entry.tier === 'notlocal'}
              <span class="dbtn" title="serve is read-only today — this will map a local clone once the human-action layer lands">map / re-discover clone</span>
            {:else}
              <span class="dbtn" title="serve is read-only today — this will pull latest once the human-action layer lands">pull latest · human-gated</span>
            {/if}
          </div>
        </aside>
      </div>
    </div>
  </div>
{/if}

<style>
  .rd-overlay {
    position: fixed;
    inset: 0;
    z-index: 61;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--phi2, 24px);
  }
  .rd-backdrop {
    position: absolute;
    inset: 0;
    background: color-mix(in srgb, var(--bg) 72%, transparent);
    backdrop-filter: blur(2px);
  }
  .rd-panel {
    position: relative;
    width: min(760px, 100%);
    max-height: 88vh;
    overflow: hidden;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 14px;
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
  }
  .rd-head {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 12px 15px;
    border-bottom: 1px solid var(--border);
    background: var(--panel-2);
  }
  .rd-head .slug {
    font-weight: 640;
    font-size: 14px;
  }
  .rd-head .role {
    font: 700 9px/1 var(--mono);
    text-transform: uppercase;
    color: var(--muted);
    border: 1px solid var(--border-2);
    border-radius: 4px;
    padding: 2px 5px;
  }
  .rd-head .clone {
    margin-left: auto;
    font-size: 11px;
    color: var(--muted);
  }
  .rd-head .clone.ok {
    color: var(--state-flight);
  }
  .rd-close {
    font: 500 11px/1 var(--mono);
    color: var(--muted);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 3px 7px;
    background: transparent;
    cursor: pointer;
  }
  .rd-close:hover {
    color: var(--text);
    border-color: var(--faint);
  }

  .rd-grid {
    display: grid;
    grid-template-columns: 1fr 260px;
    min-height: 0;
    overflow: hidden;
  }
  .rd-main {
    padding: 16px 18px;
    overflow-y: auto;
    border-right: 1px solid var(--border);
  }
  .rd-main .lab {
    display: block;
    font: 700 10px/1 var(--mono);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 10px;
  }
  .rd-main .lab .dim {
    color: var(--faint);
    text-transform: none;
    letter-spacing: normal;
    font-weight: 500;
  }
  .rd-note {
    font-size: 12px;
    color: var(--faint);
    font-style: italic;
    margin: 0;
  }
  .filerow {
    display: grid;
    grid-template-columns: 2rem 1fr auto;
    align-items: center;
    gap: 10px;
    padding: 6px 7px;
    border-radius: 7px;
    width: 100%;
    background: transparent;
    border: 0;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
  }
  .filerow:hover {
    background: var(--panel-2);
  }
  .filerow.more {
    color: var(--faint);
    cursor: default;
  }
  .filerow.more:hover {
    background: transparent;
  }
  .filerow .gutter {
    display: flex;
    align-items: flex-end;
    justify-content: flex-end;
    height: 15px;
  }
  .filerow .gutter i {
    width: 4px;
    background: var(--accent);
    opacity: 0.75;
    border-radius: 1px;
    min-height: 2px;
  }
  .filerow .path {
    font-size: 11.5px;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .filerow .conv {
    font-size: 10.5px;
    color: var(--muted);
  }
  .filerow .conv b {
    color: var(--accent);
  }

  .rd-side {
    padding: 16px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 12px;
    background: color-mix(in srgb, var(--panel-2) 55%, var(--panel));
  }
  .kv .k {
    display: block;
    font: 700 9px/1 var(--mono);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 3px;
  }
  .kv .v {
    color: var(--text);
    font-size: 12px;
  }
  .kv .v.mono {
    font-family: var(--mono);
    font-size: 11px;
    word-break: break-all;
  }
  .kv .v.ok {
    color: var(--state-flight);
  }
  .rd-actions {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 2px;
  }
  .rd-actions .btn {
    font: 500 11px/1 var(--mono);
    color: var(--muted);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 6px 8px;
    text-align: center;
    background: transparent;
    cursor: pointer;
  }
  .rd-actions .btn:hover {
    border-color: var(--accent);
    color: var(--accent);
  }
  .rd-deferred {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dashed var(--border-2);
    display: flex;
    flex-direction: column;
    gap: 5px;
    opacity: 0.6;
  }
  .rd-deferred .dbtn {
    font: 500 10.5px/1 var(--mono);
    color: var(--muted);
    border: 1px dashed var(--border-2);
    border-radius: 6px;
    padding: 5px 8px;
    text-align: center;
    cursor: not-allowed;
  }

  @media (max-width: 720px) {
    .rd-grid {
      grid-template-columns: 1fr;
    }
    .rd-main {
      border-right: none;
      border-bottom: 1px solid var(--border);
    }
  }
</style>