code-graph-cli 3.0.3

Code intelligence engine for TypeScript/JavaScript/Rust/Python/Go — query the dependency graph instead of reading source files.
<script lang="ts">
  import { onMount } from 'svelte';
  import type { ChatMessageData, CitationData } from '../types';
  import { renderMarkdown, highlightCodeBlocks } from './markdown';

  interface Props {
    message: ChatMessageData;
    onCitationClick?: (file: string, line: number) => void;
  }

  let { message, onCitationClick }: Props = $props();

  let containerEl: HTMLElement | null = $state(null);

  let renderedHtml = $derived.by(() => {
    if (message.role === 'assistant') {
      return renderMarkdown(message.content);
    }
    return '';
  });

  onMount(() => {
    if (message.role === 'assistant' && containerEl) {
      // Post-process code blocks with shiki after mount
      highlightCodeBlocks(containerEl).catch(() => {
        // Silently ignore highlighting errors
      });
    }
  });

  function handleContainerClick(e: MouseEvent) {
    const target = e.target as HTMLElement;
    const citationLink = target.closest('.citation-link') as HTMLAnchorElement | null;
    if (citationLink) {
      e.preventDefault();
      const file = citationLink.getAttribute('data-file');
      const line = parseInt(citationLink.getAttribute('data-line') || '0', 10);
      if (file) {
        onCitationClick?.(file, line);
      }
    }
  }

  // Build citation links from citations array
  let citationLinksHtml = $derived.by(() => {
    if (!message.citations || message.citations.length === 0) return '';
    const items = message.citations.map((c: CitationData) => {
      return `<a class="citation-source-link citation-link" data-file="${c.file}" data-line="${c.line}" href="#">[${c.index}] ${c.file}:${c.line} — ${c.symbol}</a>`;
    });
    return `<div class="citations-list">${items.join('')}</div>`;
  });
</script>

<div class="chat-message {message.role}">
  <div class="message-meta">
    <span class="message-author">{message.role === 'user' ? 'You' : 'Assistant'}</span>
  </div>

  {#if message.role === 'user'}
    <div class="message-bubble user-bubble">
      <span class="user-text">{message.content}</span>
    </div>
  {:else}
    <!-- eslint-disable-next-line svelte/no-at-html-tags -->
    <div
      class="message-bubble assistant-bubble markdown-content"
      bind:this={containerEl}
      onclick={handleContainerClick}
      role="presentation"
    >
      {@html renderedHtml}

      {#if message.citations && message.citations.length > 0}
        <!-- Source citations list at bottom -->
        {@html citationLinksHtml}
      {/if}
    </div>

    {#if message.toolsUsed && message.toolsUsed.length > 0}
      <div class="tools-footer">
        Used: {message.toolsUsed.join(', ')}
      </div>
    {/if}
  {/if}
</div>

<style>
  .chat-message {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 12px;
  }

  .chat-message.user {
    align-items: flex-end;
  }

  .chat-message.assistant {
    align-items: flex-start;
  }

  .message-meta {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 2px;
  }

  .message-author {
    font-size: 11px;
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
  }

  .message-bubble {
    max-width: 90%;
    border-radius: 10px;
    padding: 10px 14px;
    font-size: 13px;
    line-height: 1.6;
    word-break: break-word;
  }

  .user-bubble {
    background: var(--color-accent, #7c5cfc);
    color: white;
    border-bottom-right-radius: 3px;
  }

  .user-text {
    white-space: pre-wrap;
  }

  .assistant-bubble {
    background: var(--color-bg-elevated);
    color: var(--color-text-primary);
    border-bottom-left-radius: 3px;
    border: 1px solid var(--color-border);
  }

  /* Markdown content styles */
  .markdown-content :global(p) {
    margin: 0 0 8px 0;
  }

  .markdown-content :global(p:last-child) {
    margin-bottom: 0;
  }

  .markdown-content :global(code) {
    background: rgba(255, 255, 255, 0.08);
    padding: 1px 5px;
    border-radius: 3px;
    font-family: 'JetBrains Mono', 'Fira Code', monospace;
    font-size: 12px;
  }

  .markdown-content :global(pre) {
    background: var(--color-bg-surface);
    border: 1px solid var(--color-border);
    border-radius: 6px;
    padding: 12px;
    overflow-x: auto;
    margin: 8px 0;
  }

  .markdown-content :global(pre code) {
    background: none;
    padding: 0;
    font-size: 12px;
  }

  .markdown-content :global(ul), .markdown-content :global(ol) {
    padding-left: 20px;
    margin: 4px 0;
  }

  .markdown-content :global(li) {
    margin: 2px 0;
  }

  .markdown-content :global(strong) {
    font-weight: 600;
    color: var(--color-text-primary);
  }

  .markdown-content :global(em) {
    font-style: italic;
  }

  .markdown-content :global(blockquote) {
    border-left: 3px solid var(--color-accent, #7c5cfc);
    margin: 8px 0;
    padding-left: 12px;
    color: var(--color-text-muted);
  }

  .markdown-content :global(a.citation-link) {
    color: var(--color-accent, #7c5cfc);
    text-decoration: none;
    cursor: pointer;
    font-size: 11px;
    vertical-align: super;
  }

  .markdown-content :global(a.citation-link:hover) {
    text-decoration: underline;
  }

  /* Citations list at bottom of assistant message */
  .markdown-content :global(.citations-list) {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--color-border);
  }

  .markdown-content :global(.citation-source-link) {
    font-size: 11px;
    color: var(--color-text-muted);
    text-decoration: none;
    cursor: pointer;
    font-family: 'JetBrains Mono', monospace;
    display: block;
    padding: 2px 0;
    transition: color 100ms ease;
  }

  .markdown-content :global(.citation-source-link:hover) {
    color: var(--color-accent, #7c5cfc);
  }

  /* Tools footer */
  .tools-footer {
    font-size: 11px;
    color: var(--color-text-muted);
    padding: 2px 0;
    font-style: italic;
    max-width: 90%;
  }
</style>