Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
omni-code / tui / src / views / LogViewer.tsx
Size: Mime:
import React, { useCallback, useEffect, useState } from "react";
import { Box, Text, useInput, useStdout, type Key } from "ink";
import { api } from "../rpc.js";
import { Panel } from "../components/Panel.js";
import { HelpOverlay } from "../components/HelpOverlay.js";
import type { Hint } from "../components/KeyHint.js";
import { useTheme } from "../ThemeContext.js";

type HistoryMessage = {
  index: number;
  role: string;
  content: string;
};

type Props = {
  ticketId: string;
  ticketTitle: string;
  projectId: string;
  onBack: () => void;
  onQuit: () => void;
  active?: boolean;
  embedded?: boolean;
  onHints?: (hints: Hint[]) => void;
  onStatus?: (status: string | null) => void;
};

const HELP_HINTS: Hint[] = [
  { key: "j/k", label: "navigate messages" },
  { key: "g", label: "jump to top" },
  { key: "G", label: "jump to bottom" },
  { key: "r", label: "refresh" },
  { key: "?", label: "help" },
  { key: "esc", label: "back to ticket" },
];

const BAR_HINTS: Hint[] = [
  { key: "j/k", label: "navigate" },
  { key: "g/G", label: "top/bottom" },
  { key: "r", label: "refresh" },
  { key: "?", label: "help" },
  { key: "esc", label: "back" },
];

export function LogViewer({ ticketId, ticketTitle, projectId, onBack, onQuit, active = true, embedded = false, onHints, onStatus }: Props) {
  const t = useTheme();
  const roleColors: Record<string, string> = {
    user: t.roleUser,
    assistant: t.roleAssistant,
    tool: t.roleTool,
    system: t.roleSystem,
  };

  const [messages, setMessages] = useState<HistoryMessage[]>([]);
  const [selected, setSelected] = useState(0);
  const [scrollOffset, setScrollOffset] = useState(0);
  const [status, setStatus] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [showHelp, setShowHelp] = useState(false);
  const { stdout } = useStdout();
  const visibleRows = (stdout?.rows ?? 24) - 6;

  const refresh = useCallback(async () => {
    try {
      const result = await api.ticketHistory(ticketId, projectId, 200);
      setMessages(result);
      setLoading(false);
    } catch (err) {
      setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`);
      setLoading(false);
    }
  }, [ticketId, projectId]);

  useEffect(() => {
    refresh();
    const interval = setInterval(refresh, 10000);
    return () => clearInterval(interval);
  }, [refresh]);

  useEffect(() => {
    if (!status) return;
    const timer = setTimeout(() => setStatus(null), 4000);
    return () => clearTimeout(timer);
  }, [status]);

  useEffect(() => { onStatus?.(status ?? null); }, [status]);
  useEffect(() => { onHints?.(BAR_HINTS); }, []);

  useInput((input: string, key: Key) => {
    if (!active) return;
    if (showHelp) return;
    if (input === "?") { setShowHelp(true); return; }
    if (key.escape) { onBack(); return; }
    if (input === "q") { onQuit(); return; }
    if (input === "r") { setLoading(true); refresh(); setStatus("Refreshed"); return; }
    if ((key.upArrow || input === "k") && messages.length > 0) {
      setSelected((v) => {
        const next = Math.max(0, v - 1);
        if (next < scrollOffset) setScrollOffset(next);
        return next;
      });
      return;
    }
    if ((key.downArrow || input === "j") && messages.length > 0) {
      setSelected((v) => {
        const next = Math.min(messages.length - 1, v + 1);
        if (next >= scrollOffset + visibleRows) setScrollOffset(next - visibleRows + 1);
        return next;
      });
      return;
    }
    if (input === "G") {
      const last = messages.length - 1;
      setSelected(last);
      setScrollOffset(Math.max(0, last - visibleRows + 1));
      return;
    }
    if (input === "g") { setSelected(0); setScrollOffset(0); return; }
  });

  if (showHelp) {
    return (
      <Box flexDirection="column" flexGrow={1}>
        <HelpOverlay title="Log Viewer" hints={HELP_HINTS} onClose={() => setShowHelp(false)} />
      </Box>
    );
  }

  const visibleMessages = messages.slice(scrollOffset, scrollOffset + visibleRows);

  const content = (
    <Box flexDirection="column" paddingX={1} flexGrow={1}>
      {loading && messages.length === 0 ? (
        <Text color={t.fgSubtle}>Loading history...</Text>
      ) : messages.length === 0 ? (
        <Text color={t.fgSubtle}>No history found.</Text>
      ) : (
        visibleMessages.map((msg, i) => {
          const actualIndex = scrollOffset + i;
          const isSelected = actualIndex === selected;
          const roleColor = roleColors[msg.role] ?? t.fg;
          const preview = isSelected
            ? msg.content
            : msg.content.length > 120 ? msg.content.slice(0, 120) + "\u2026" : msg.content;
          const lines = preview.split("\n");
          const displayContent = isSelected ? lines.join("\n") : lines[0] + (lines.length > 1 ? " \u2026" : "");

          return (
            <Text key={msg.index} color={isSelected ? t.accent : undefined} wrap="truncate">
              {isSelected ? "\u25B8 " : "  "}
              <Text color={roleColor} bold>[{msg.role}]</Text>
              {" "}{displayContent}
            </Text>
          );
        })
      )}
    </Box>
  );

  if (embedded) return content;

  return (
    <Box flexDirection="column" flexGrow={1}>
      <Panel title={`Log \u2014 ${ticketTitle} (${messages.length})`} focused flexGrow={1}>
        {content}
      </Panel>
    </Box>
  );
}