mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
- Add SessionConfigSchema with persistIndefinitely, maxHistoryMessages, saveInputHistory - Implement Session.truncateHistory() method for limiting message history - Update HandleMessage to support history truncation and input history toggle - Add config flow through useSession and App components - Add 19 unit tests for SessionConfigSchema - Update CHANGELOG.md and ROADMAP.md for v0.22.2
301 lines
9.4 KiB
TypeScript
301 lines
9.4 KiB
TypeScript
/**
|
|
* 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 { Config } from "../shared/constants/config.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"
|
|
import { ringBell } from "./utils/bell.js"
|
|
|
|
export interface AppDependencies {
|
|
storage: IStorage
|
|
sessionStorage: ISessionStorage
|
|
llm: ILLMClient
|
|
tools: IToolRegistry
|
|
projectStructure?: ProjectStructure
|
|
config?: Config
|
|
}
|
|
|
|
export interface ExtendedAppProps extends AppProps {
|
|
deps: AppDependencies
|
|
onExit?: () => void
|
|
multiline?: boolean | "auto"
|
|
syntaxHighlight?: boolean
|
|
theme?: "dark" | "light"
|
|
showStats?: boolean
|
|
showToolCalls?: boolean
|
|
bellOnComplete?: boolean
|
|
}
|
|
|
|
function LoadingScreen(): React.JSX.Element {
|
|
return (
|
|
<Box flexDirection="column" padding={1}>
|
|
<Text color="cyan">Loading session...</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
function ErrorScreen({ error }: { error: Error }): React.JSX.Element {
|
|
return (
|
|
<Box flexDirection="column" padding={1}>
|
|
<Text color="red" bold>
|
|
Error
|
|
</Text>
|
|
<Text color="red">{error.message}</Text>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
async function handleErrorDefault(_error: Error): Promise<ErrorOption> {
|
|
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,
|
|
theme = "dark",
|
|
showStats = true,
|
|
showToolCalls = true,
|
|
bellOnComplete = false,
|
|
}: ExtendedAppProps): React.JSX.Element {
|
|
const { exit } = useApp()
|
|
|
|
const [branch] = useState<BranchInfo>({ name: "main", isDetached: false })
|
|
const [sessionTime, setSessionTime] = useState("0m")
|
|
const [autoApply, setAutoApply] = useState(initialAutoApply)
|
|
const [commandResult, setCommandResult] = useState<CommandResult | null>(null)
|
|
const [pendingConfirmation, setPendingConfirmation] = useState<PendingConfirmation | null>(null)
|
|
|
|
const projectName = projectPath.split("/").pop() ?? "unknown"
|
|
|
|
const handleConfirmation = useCallback(
|
|
async (message: string, diff?: DiffInfo): Promise<boolean | ConfirmationResult> => {
|
|
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,
|
|
config: deps.config,
|
|
},
|
|
{
|
|
autoApply,
|
|
onConfirmation: handleConfirmation,
|
|
onError: handleErrorDefault,
|
|
},
|
|
)
|
|
|
|
const reindex = useCallback(async (): Promise<void> => {
|
|
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])
|
|
|
|
useEffect(() => {
|
|
if (bellOnComplete && status === "ready") {
|
|
ringBell()
|
|
}
|
|
}, [bellOnComplete, status])
|
|
|
|
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 <LoadingScreen />
|
|
}
|
|
|
|
if (error) {
|
|
return <ErrorScreen error={error} />
|
|
}
|
|
|
|
const isInputDisabled = status === "thinking" || status === "tool_call" || !!pendingConfirmation
|
|
|
|
return (
|
|
<Box flexDirection="column" height="100%">
|
|
<StatusBar
|
|
contextUsage={session?.context.tokenUsage ?? 0}
|
|
projectName={projectName}
|
|
branch={branch}
|
|
sessionTime={sessionTime}
|
|
status={status}
|
|
theme={theme}
|
|
/>
|
|
<Chat
|
|
messages={messages}
|
|
isThinking={status === "thinking"}
|
|
theme={theme}
|
|
showStats={showStats}
|
|
showToolCalls={showToolCalls}
|
|
/>
|
|
{commandResult && (
|
|
<Box
|
|
borderStyle="round"
|
|
borderColor={commandResult.success ? "green" : "red"}
|
|
paddingX={1}
|
|
marginY={1}
|
|
>
|
|
<Text color={commandResult.success ? "green" : "red"} wrap="wrap">
|
|
{commandResult.message}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
{pendingConfirmation && (
|
|
<ConfirmDialog
|
|
message={pendingConfirmation.message}
|
|
diff={
|
|
pendingConfirmation.diff
|
|
? {
|
|
filePath: pendingConfirmation.diff.filePath,
|
|
oldLines: pendingConfirmation.diff.oldLines,
|
|
newLines: pendingConfirmation.diff.newLines,
|
|
startLine: pendingConfirmation.diff.startLine,
|
|
}
|
|
: undefined
|
|
}
|
|
onSelect={handleConfirmSelect}
|
|
editableContent={pendingConfirmation.diff?.newLines}
|
|
syntaxHighlight={syntaxHighlight}
|
|
/>
|
|
)}
|
|
<Input
|
|
onSubmit={handleSubmit}
|
|
history={session?.inputHistory ?? []}
|
|
disabled={isInputDisabled}
|
|
placeholder={isInputDisabled ? "Processing..." : "Type a message..."}
|
|
storage={deps.storage}
|
|
projectRoot={projectPath}
|
|
autocompleteEnabled={true}
|
|
multiline={multiline}
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|