litvc 1.3.1

Lit - The agentic-first distributed version control system. A complete Git replacement designed for AI agents first and humans second.
Documentation
<script lang="ts">
  import type { BranchEntry } from "../types";
  import { checkoutBranch, createBranch, deleteBranch, stackList, errMsg } from "../tauri";
  import type { StackEntry } from "../types";

  let {
    branches,
    currentBranch,
    onRefresh,
    onError,
    showStacks = false,
  }: {
    branches: BranchEntry[];
    currentBranch: string | null;
    onRefresh: () => void;
    onError?: (message: string) => void;
    showStacks?: boolean;
  } = $props();

  let newBranchName = $state("");
  let showNewBranch = $state(false);
  let stacks: StackEntry[] = $state([]);
  let filter = $state("");

  let filteredBranches = $derived(
    filter
      ? branches.filter((b) => b.name.toLowerCase().includes(filter.toLowerCase()))
      : branches
  );

  async function handleCheckout(name: string) {
    try {
      await checkoutBranch(name);
      onRefresh();
    } catch (e) {
      onError?.(errMsg(e));
    }
  }

  async function handleCreate(e: Event) {
    e.preventDefault();
    if (!newBranchName.trim()) return;
    try {
      await createBranch(newBranchName.trim());
      newBranchName = "";
      showNewBranch = false;
      onRefresh();
    } catch (e) {
      onError?.(errMsg(e));
    }
  }

  async function handleDelete(name: string) {
    try {
      await deleteBranch(name);
      onRefresh();
    } catch (e) {
      onError?.(errMsg(e));
    }
  }

  async function loadStacks() {
    try {
      const result = await stackList();
      stacks = result.stacks ?? [];
    } catch (e) {
      onError?.(errMsg(e));
    }
  }

  $effect(() => {
    if (showStacks) loadStacks();
  });
</script>

<div class="branch-panel">
  <div class="panel-header">
    <span class="panel-title">{showStacks ? "Stacks" : "Branches"}</span>
    <button class="btn-sm" title="New branch" onclick={() => (showNewBranch = !showNewBranch)}>
      <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
        <path d="M8 2a.75.75 0 0 1 .75.75v4.5h4.5a.75.75 0 0 1 0 1.5h-4.5v4.5a.75.75 0 0 1-1.5 0v-4.5h-4.5a.75.75 0 0 1 0-1.5h4.5v-4.5A.75.75 0 0 1 8 2z"/>
      </svg>
    </button>
  </div>

  {#if showNewBranch}
    <form class="new-branch-form" onsubmit={handleCreate}>
      <input
        type="text"
        placeholder="Branch name..."
        bind:value={newBranchName}
        class="branch-input"
      />
      <button type="submit" class="btn-create" disabled={!newBranchName.trim()}>Create</button>
    </form>
  {/if}

  <div class="filter-box">
    <input type="text" placeholder="Filter..." bind:value={filter} class="filter-input" />
  </div>

  <div class="branch-list">
    {#if showStacks && stacks.length > 0}
      <div class="section-label">Stacked Branches</div>
      {#each stacks as stack}
        <button
          class="branch-item"
          class:current={stack.branch === currentBranch}
          onclick={() => handleCheckout(stack.branch)}
        >
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
            <path d="M12 2L2 7l10 5 10-5-10-5z"/>
            <path d="M2 17l10 5 10-5"/>
          </svg>
          <span class="branch-name truncate">{stack.branch}</span>
          <span class="stack-base text-mute">← {stack.base}</span>
        </button>
      {/each}
      <div class="section-label" style="margin-top: var(--space-12);">All Branches</div>
    {/if}

    {#each filteredBranches as branch}
      <div class="branch-item-row">
        <button
          class="branch-item"
          class:current={branch.is_current}
          onclick={() => handleCheckout(branch.name)}
        >
          <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" class="branch-icon">
            <path d="M11.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zm-2.25.75a2.25 2.25 0 1 1 3 2.122V6.5a1.5 1.5 0 0 1-1.5 1.5H9.5a1.5 1.5 0 0 0-1.5 1.5v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A3 3 0 0 1 9.5 6.5H11a.5.5 0 0 0 .5-.5v-1.128A2.251 2.251 0 0 1 9.5 3.25zM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zM4.25 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5z"/>
          </svg>
          <span class="branch-name truncate">{branch.name}</span>
          {#if branch.is_current}
            <span class="current-badge">HEAD</span>
          {/if}
        </button>
        {#if !branch.is_current}
          <button class="btn-delete" title="Delete branch" onclick={() => handleDelete(branch.name)}>
            <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
              <path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/>
            </svg>
          </button>
        {/if}
      </div>
    {:else}
      <div class="empty-text">No branches found</div>
    {/each}
  </div>
</div>

<style>
  .branch-panel {
    height: 100%;
    display: flex;
    flex-direction: column;
    background: var(--bg-1);
  }

  .panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-10) var(--space-12);
    border-bottom: 1px solid var(--border-1);
    flex-shrink: 0;
  }
  .panel-title {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-3);
  }

  .btn-sm {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: var(--radius-sm);
    color: var(--text-3);
    transition: all var(--transition-fast);
  }
  .btn-sm:hover {
    background: var(--bg-hover);
    color: var(--text-1);
  }

  .new-branch-form {
    display: flex;
    gap: var(--space-6);
    padding: var(--space-8) var(--space-12);
    border-bottom: 1px solid var(--border-1);
  }
  .branch-input {
    flex: 1;
    font-size: var(--text-sm);
    padding: var(--space-4) var(--space-8);
  }
  .btn-create {
    padding: var(--space-4) var(--space-10);
    background: var(--accent);
    color: #fff;
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    font-weight: 500;
    transition: background var(--transition-fast);
  }
  .btn-create:hover:not(:disabled) {
    background: var(--accent-hover);
  }
  .btn-create:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .filter-box {
    padding: var(--space-8) var(--space-12);
  }
  .filter-input {
    width: 100%;
    font-size: var(--text-sm);
    padding: var(--space-4) var(--space-8);
  }

  .branch-list {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-4) var(--space-8);
  }

  .section-label {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-mute);
    padding: var(--space-6) var(--space-8);
  }

  .branch-item-row {
    display: flex;
    align-items: center;
  }

  .branch-item {
    display: flex;
    align-items: center;
    gap: var(--space-8);
    width: 100%;
    padding: var(--space-6) var(--space-8);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    color: var(--text-2);
    text-align: left;
    transition: all var(--transition-fast);
  }
  .branch-item:hover {
    background: var(--bg-hover);
    color: var(--text-1);
  }
  .branch-item.current {
    background: var(--accent-soft);
    color: var(--accent-text);
  }
  .branch-item.current .branch-icon {
    color: var(--accent);
  }

  .branch-name {
    flex: 1;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
  }

  .current-badge {
    font-size: 9px;
    font-weight: 700;
    text-transform: uppercase;
    padding: 1px 5px;
    border-radius: 3px;
    background: var(--accent);
    color: #fff;
    letter-spacing: 0.05em;
  }

  .stack-base {
    font-size: var(--text-xs);
    font-family: var(--font-mono);
  }

  .btn-delete {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: var(--radius-sm);
    color: var(--text-mute);
    opacity: 0;
    transition: all var(--transition-fast);
  }
  .branch-item-row:hover .btn-delete {
    opacity: 1;
  }
  .btn-delete:hover {
    background: var(--danger-soft);
    color: var(--danger-text);
  }

  .empty-text {
    text-align: center;
    color: var(--text-mute);
    font-size: var(--text-sm);
    padding: var(--space-20);
  }
</style>