/**
* 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 && (
)}
)
}