import { Context, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextTrigger, } from "@/components/ai-elements/context"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; import { useChat, type UIMessage } from "@ai-sdk/react"; import { Add01Icon, AlertCircleIcon, ArrowDown01Icon, Cancel01Icon, Delete02Icon, FilterIcon, TerminalIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { motion } from "motion/react"; import { useEffect, useMemo } from "react"; import { estimateCost, getModel, getModelContextLimit } from "../config"; import type { SessionMeta } from "../lib/sessions"; import { useAgentsStore } from "../store/agentsStore"; import { getOrCreateChat, useChatStore } from "../store/chatStore"; import { usePlanStore } from "../store/planStore"; import { AgentSwitcher } from "./AgentSwitcher"; import { AiChatView } from "./AiChat"; import { PlanDiffReview } from "./PlanDiffReview"; import { TodoStrip } from "./TodoStrip"; const SUGGESTIONS = [ { label: "Explain the last error", hint: "Read the terminal buffer", icon: AlertCircleIcon, text: "Explain the last error in the terminal.", }, { label: "Generate a command", hint: "Tell me what you want to do", icon: TerminalIcon, text: "Give me a command to ", }, { label: "Summarize buffer", hint: "Recap recent activity", icon: FilterIcon, text: "Summarize what just happened in the terminal.", }, ]; export function AiMiniWindow() { const closeMini = useChatStore((s) => s.closeMini); const sessionId = useChatStore((s) => s.activeSessionId); const openPanel = useChatStore((s) => s.openPanel); const expandToPanel = () => { closeMini(); openPanel(); }; useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { const target = e.target as HTMLElement | null; const tag = target?.tagName; if (tag === "INPUT" || tag === "TEXTAREA") return; closeMini(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [closeMini]); return (
{sessionId ? ( ) : ( )} ); } function Body({ sessionId, onClose, onExpand, }: { sessionId: string; onClose: () => void; onExpand: () => void; }) { const focusInput = useChatStore((s) => s.focusInput); const step = useChatStore((s) => s.agentMeta.step); const chat = useMemo(() => getOrCreateChat(sessionId), [sessionId]); const helpers = useChat({ chat }); const isBusy = helpers.status === "submitted" || helpers.status === "streaming"; return ( <>
{helpers.messages.length === 0 ? ( ) : (
)}
); } function PlanModeStrip() { const active = usePlanStore((s) => s.active); const queueLen = usePlanStore((s) => s.queue.length); const disable = usePlanStore((s) => s.disable); if (!active) return null; return (
Plan mode {queueLen > 0 ? `· ${queueLen} queued` : "· no edits queued"}
); } function EmptyShell({ onClose, onExpand, }: { onClose: () => void; onExpand: () => void; }) { return ( <>
Loading sessions…
); } function Header({ step, isBusy, onClose, messages, }: { step: string | null; isBusy: boolean; onClose: () => void; onExpand: () => void; messages?: UIMessage[]; }) { const customAgents = useAgentsStore((s) => s.customAgents); void customAgents; return (
{messages !== undefined ? ( ) : null}
{isBusy ? ( {step ?? "Thinking…"} ) : null}
); } function estimateTokens(messages: UIMessage[]): number { let chars = 0; for (const m of messages) { for (const p of m.parts) { if (p.type === "text") { chars += (p as { text?: string }).text?.length ?? 0; } else if (p.type === "reasoning") { chars += (p as { text?: string }).text?.length ?? 0; } else if (typeof p.type === "string" && p.type.startsWith("tool-")) { const tp = p as unknown as { input?: unknown; output?: unknown }; if (tp.input) chars += JSON.stringify(tp.input).length; if (tp.output) chars += JSON.stringify(tp.output).length; } } } return Math.ceil(chars / 4); } function formatTokens(n: number): string { if (n < 1000) return String(n); if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`; return `${(n / 1_000_000).toFixed(2)}M`; } function ContextIndicator({ messages }: { messages: UIMessage[] }) { const modelId = useChatStore((s) => s.selectedModelId); const tokens = useChatStore((s) => s.agentMeta.tokens); const estimated = useMemo(() => estimateTokens(messages), [messages]); const reported = tokens.inputTokens + tokens.outputTokens; const used = reported > 0 ? tokens.inputTokens : estimated; const max = getModelContextLimit(modelId); const modelLabel = useMemo(() => { try { return getModel(modelId).label; } catch { return modelId; } }, [modelId]); const cost = estimateCost(modelId, tokens); const cacheRate = tokens.inputTokens > 0 ? Math.round((tokens.cachedInputTokens / tokens.inputTokens) * 100) : 0; return (
Model {modelLabel}
{reported > 0 ? "Input" : "Estimated input"} {formatTokens(used)}
{reported > 0 && ( <>
Output {formatTokens(tokens.outputTokens)}
{tokens.cachedInputTokens > 0 && (
Cache hit {cacheRate}%
)} {cost != null && (
Session cost ${cost.toFixed(cost < 0.01 ? 4 : cost < 1 ? 3 : 2)}
)} )}
Window {formatTokens(max)}
{reported > 0 ? "Reported by provider; cost is an estimate." : "Token count is approximate (chars / 4)."}
); } function SessionPicker() { const sessions = useChatStore((s) => s.sessions); const activeId = useChatStore((s) => s.activeSessionId); const switchSession = useChatStore((s) => s.switchSession); const newSession = useChatStore((s) => s.newSession); const deleteSession = useChatStore((s) => s.deleteSession); const active = sessions.find((s) => s.id === activeId) ?? null; if (!active) return null; const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt); return ( newSession()} className="gap-2 text-xs" > New session {sorted.length > 0 ? : null} {sorted.map((s) => ( switchSession(s.id)} onDelete={() => deleteSession(s.id)} /> ))} ); } function SessionRow({ session, active, onSelect, onDelete, }: { session: SessionMeta; active: boolean; onSelect: () => void; onDelete: () => void; }) { return ( { // Don't dismiss if user clicked the trash icon — handle below. const target = e.target as HTMLElement | null; if (target?.closest("[data-session-delete]")) { e.preventDefault(); return; } onSelect(); }} className={cn( "group flex items-center justify-between gap-2 text-xs", active && "bg-accent/40", )} > {session.title || "New chat"} ); } function EmptyState({ onPick }: { onPick: (text: string) => void }) { return (
Terax

Ask Terax anything

Terax sees the active terminal — cwd, recent commands, and output.

{SUGGESTIONS.map((s) => ( ))}
); }