/** * Main TUI App component. * Orchestrates the terminal user interface. */ import { Box, Text, useApp } from "ink" import React, { useCallback, useEffect, useState } from "react" import type { ILLMClient } from "../domain/services/ILLMClient.js" import type { ISessionStorage } from "../domain/services/ISessionStorage.js" import type { IStorage } from "../domain/services/IStorage.js" import type { DiffInfo } from "../domain/services/ITool.js" import type { ErrorOption } from "../shared/errors/IpuaroError.js" import type { IToolRegistry } from "../application/interfaces/IToolRegistry.js" import type { ConfirmationResult } from "../application/use-cases/ExecuteTool.js" import type { ProjectStructure } from "../infrastructure/llm/prompts.js" import { Chat, ConfirmDialog, Input, StatusBar } from "./components/index.js" import { type CommandResult, useCommands, useHotkeys, useSession } from "./hooks/index.js" import type { AppProps, BranchInfo } from "./types.js" import type { ConfirmChoice } from "../shared/types/index.js" export interface AppDependencies { storage: IStorage sessionStorage: ISessionStorage llm: ILLMClient tools: IToolRegistry projectStructure?: ProjectStructure } export interface ExtendedAppProps extends AppProps { deps: AppDependencies onExit?: () => void multiline?: boolean | "auto" syntaxHighlight?: boolean } function LoadingScreen(): React.JSX.Element { return ( Loading session... ) } function ErrorScreen({ error }: { error: Error }): React.JSX.Element { return ( Error {error.message} ) } async function handleErrorDefault(_error: Error): Promise { return Promise.resolve("skip") } interface PendingConfirmation { message: string diff?: DiffInfo resolve: (result: boolean | ConfirmationResult) => void } export function App({ projectPath, autoApply: initialAutoApply = false, deps, onExit, multiline = false, syntaxHighlight = true, }: ExtendedAppProps): React.JSX.Element { const { exit } = useApp() const [branch] = useState({ name: "main", isDetached: false }) const [sessionTime, setSessionTime] = useState("0m") const [autoApply, setAutoApply] = useState(initialAutoApply) const [commandResult, setCommandResult] = useState(null) const [pendingConfirmation, setPendingConfirmation] = useState(null) const projectName = projectPath.split("/").pop() ?? "unknown" const handleConfirmation = useCallback( async (message: string, diff?: DiffInfo): Promise => { return new Promise((resolve) => { setPendingConfirmation({ message, diff, resolve }) }) }, [], ) const handleConfirmSelect = useCallback( (choice: ConfirmChoice, editedContent?: string[]) => { if (!pendingConfirmation) { return } if (choice === "apply") { if (editedContent) { pendingConfirmation.resolve({ confirmed: true, editedContent }) } else { pendingConfirmation.resolve(true) } } else { pendingConfirmation.resolve(false) } setPendingConfirmation(null) }, [pendingConfirmation], ) const { session, messages, status, isLoading, error, sendMessage, undo, clearHistory, abort } = useSession( { storage: deps.storage, sessionStorage: deps.sessionStorage, llm: deps.llm, tools: deps.tools, projectRoot: projectPath, projectName, projectStructure: deps.projectStructure, }, { autoApply, onConfirmation: handleConfirmation, onError: handleErrorDefault, }, ) const reindex = useCallback(async (): Promise => { const { IndexProject } = await import("../application/use-cases/IndexProject.js") const indexProject = new IndexProject(deps.storage, projectPath) await indexProject.execute(projectPath) }, [deps.storage, projectPath]) const { executeCommand, isCommand } = useCommands( { session, sessionStorage: deps.sessionStorage, storage: deps.storage, llm: deps.llm, tools: deps.tools, projectRoot: projectPath, projectName, }, { clearHistory, undo, setAutoApply, reindex, }, { autoApply }, ) const handleExit = useCallback((): void => { onExit?.() exit() }, [exit, onExit]) const handleInterrupt = useCallback((): void => { if (status === "thinking" || status === "tool_call") { abort() } }, [status, abort]) const handleUndo = useCallback((): void => { void undo() }, [undo]) useHotkeys( { onInterrupt: handleInterrupt, onExit: handleExit, onUndo: handleUndo, }, { enabled: !isLoading }, ) useEffect(() => { if (!session) { return } const interval = setInterval(() => { setSessionTime(session.getSessionDurationFormatted()) }, 60_000) setSessionTime(session.getSessionDurationFormatted()) return (): void => { clearInterval(interval) } }, [session]) const handleSubmit = useCallback( (text: string): void => { if (isCommand(text)) { void executeCommand(text).then((result) => { setCommandResult(result) // Auto-clear command result after 5 seconds setTimeout(() => { setCommandResult(null) }, 5000) }) return } void sendMessage(text) }, [sendMessage, isCommand, executeCommand], ) if (isLoading) { return } if (error) { return } const isInputDisabled = status === "thinking" || status === "tool_call" || !!pendingConfirmation return ( {commandResult && ( {commandResult.message} )} {pendingConfirmation && ( )} ) }