mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat(ipuaro): add display configuration
Add DisplayConfigSchema with theme support (dark/light), stats/tool calls visibility toggles, bell notification on completion, and progress bar control. Includes theme utilities with dynamic color schemes and 46 new tests.
This commit is contained in:
@@ -86,6 +86,17 @@ export const InputConfigSchema = z.object({
|
||||
multiline: z.union([z.boolean(), z.literal("auto")]).default(false),
|
||||
})
|
||||
|
||||
/**
|
||||
* Display configuration schema.
|
||||
*/
|
||||
export const DisplayConfigSchema = z.object({
|
||||
showStats: z.boolean().default(true),
|
||||
showToolCalls: z.boolean().default(true),
|
||||
theme: z.enum(["dark", "light"]).default("dark"),
|
||||
bellOnComplete: z.boolean().default(false),
|
||||
progressBar: z.boolean().default(true),
|
||||
})
|
||||
|
||||
/**
|
||||
* Full configuration schema.
|
||||
*/
|
||||
@@ -97,6 +108,7 @@ export const ConfigSchema = z.object({
|
||||
undo: UndoConfigSchema.default({}),
|
||||
edit: EditConfigSchema.default({}),
|
||||
input: InputConfigSchema.default({}),
|
||||
display: DisplayConfigSchema.default({}),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -110,6 +122,7 @@ export type WatchdogConfig = z.infer<typeof WatchdogConfigSchema>
|
||||
export type UndoConfig = z.infer<typeof UndoConfigSchema>
|
||||
export type EditConfig = z.infer<typeof EditConfigSchema>
|
||||
export type InputConfig = z.infer<typeof InputConfigSchema>
|
||||
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
||||
|
||||
/**
|
||||
* Default configuration.
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
@@ -31,6 +32,10 @@ export interface ExtendedAppProps extends AppProps {
|
||||
onExit?: () => void
|
||||
multiline?: boolean | "auto"
|
||||
syntaxHighlight?: boolean
|
||||
theme?: "dark" | "light"
|
||||
showStats?: boolean
|
||||
showToolCalls?: boolean
|
||||
bellOnComplete?: boolean
|
||||
}
|
||||
|
||||
function LoadingScreen(): React.JSX.Element {
|
||||
@@ -69,6 +74,10 @@ export function App({
|
||||
onExit,
|
||||
multiline = false,
|
||||
syntaxHighlight = true,
|
||||
theme = "dark",
|
||||
showStats = true,
|
||||
showToolCalls = true,
|
||||
bellOnComplete = false,
|
||||
}: ExtendedAppProps): React.JSX.Element {
|
||||
const { exit } = useApp()
|
||||
|
||||
@@ -193,6 +202,12 @@ export function App({
|
||||
}
|
||||
}, [session])
|
||||
|
||||
useEffect(() => {
|
||||
if (bellOnComplete && status === "ready") {
|
||||
ringBell()
|
||||
}
|
||||
}, [bellOnComplete, status])
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(text: string): void => {
|
||||
if (isCommand(text)) {
|
||||
@@ -228,8 +243,15 @@ export function App({
|
||||
branch={branch}
|
||||
sessionTime={sessionTime}
|
||||
status={status}
|
||||
theme={theme}
|
||||
/>
|
||||
<Chat
|
||||
messages={messages}
|
||||
isThinking={status === "thinking"}
|
||||
theme={theme}
|
||||
showStats={showStats}
|
||||
showToolCalls={showToolCalls}
|
||||
/>
|
||||
<Chat messages={messages} isThinking={status === "thinking"} />
|
||||
{commandResult && (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
|
||||
@@ -7,10 +7,14 @@ import { Box, Text } from "ink"
|
||||
import type React from "react"
|
||||
import type { ChatMessage } from "../../domain/value-objects/ChatMessage.js"
|
||||
import type { ToolCall } from "../../domain/value-objects/ToolCall.js"
|
||||
import { getRoleColor, type Theme } from "../utils/theme.js"
|
||||
|
||||
export interface ChatProps {
|
||||
messages: ChatMessage[]
|
||||
isThinking: boolean
|
||||
theme?: Theme
|
||||
showStats?: boolean
|
||||
showToolCalls?: boolean
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp: number): string {
|
||||
@@ -42,11 +46,20 @@ function formatToolCall(call: ToolCall): string {
|
||||
return `[${call.name} ${params}]`
|
||||
}
|
||||
|
||||
function UserMessage({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
interface MessageComponentProps {
|
||||
message: ChatMessage
|
||||
theme: Theme
|
||||
showStats: boolean
|
||||
showToolCalls: boolean
|
||||
}
|
||||
|
||||
function UserMessage({ message, theme }: MessageComponentProps): React.JSX.Element {
|
||||
const roleColor = getRoleColor("user", theme)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box gap={1}>
|
||||
<Text color="green" bold>
|
||||
<Text color={roleColor} bold>
|
||||
You
|
||||
</Text>
|
||||
<Text color="gray" dimColor>
|
||||
@@ -60,13 +73,19 @@ function UserMessage({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
function AssistantMessage({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
function AssistantMessage({
|
||||
message,
|
||||
theme,
|
||||
showStats,
|
||||
showToolCalls,
|
||||
}: MessageComponentProps): React.JSX.Element {
|
||||
const stats = formatStats(message.stats)
|
||||
const roleColor = getRoleColor("assistant", theme)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box gap={1}>
|
||||
<Text color="cyan" bold>
|
||||
<Text color={roleColor} bold>
|
||||
Assistant
|
||||
</Text>
|
||||
<Text color="gray" dimColor>
|
||||
@@ -74,7 +93,7 @@ function AssistantMessage({ message }: { message: ChatMessage }): React.JSX.Elem
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{message.toolCalls && message.toolCalls.length > 0 && (
|
||||
{showToolCalls && message.toolCalls && message.toolCalls.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2} marginBottom={1}>
|
||||
{message.toolCalls.map((call) => (
|
||||
<Text key={call.id} color="yellow">
|
||||
@@ -90,7 +109,7 @@ function AssistantMessage({ message }: { message: ChatMessage }): React.JSX.Elem
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{stats && (
|
||||
{showStats && stats && (
|
||||
<Box marginLeft={2} marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
{stats}
|
||||
@@ -101,7 +120,9 @@ function AssistantMessage({ message }: { message: ChatMessage }): React.JSX.Elem
|
||||
)
|
||||
}
|
||||
|
||||
function ToolMessage({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
function ToolMessage({ message, theme }: MessageComponentProps): React.JSX.Element {
|
||||
const roleColor = getRoleColor("tool", theme)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
||||
{message.toolResults?.map((result) => (
|
||||
@@ -115,31 +136,39 @@ function ToolMessage({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
function SystemMessage({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
function SystemMessage({ message, theme }: MessageComponentProps): React.JSX.Element {
|
||||
const isError = message.content.toLowerCase().startsWith("error")
|
||||
const roleColor = getRoleColor("system", theme)
|
||||
|
||||
return (
|
||||
<Box marginBottom={1} marginLeft={2}>
|
||||
<Text color={isError ? "red" : "gray"} dimColor={!isError}>
|
||||
<Text color={isError ? "red" : roleColor} dimColor={!isError}>
|
||||
{message.content}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function MessageComponent({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
function MessageComponent({
|
||||
message,
|
||||
theme,
|
||||
showStats,
|
||||
showToolCalls,
|
||||
}: MessageComponentProps): React.JSX.Element {
|
||||
const props = { message, theme, showStats, showToolCalls }
|
||||
|
||||
switch (message.role) {
|
||||
case "user": {
|
||||
return <UserMessage message={message} />
|
||||
return <UserMessage {...props} />
|
||||
}
|
||||
case "assistant": {
|
||||
return <AssistantMessage message={message} />
|
||||
return <AssistantMessage {...props} />
|
||||
}
|
||||
case "tool": {
|
||||
return <ToolMessage message={message} />
|
||||
return <ToolMessage {...props} />
|
||||
}
|
||||
case "system": {
|
||||
return <SystemMessage message={message} />
|
||||
return <SystemMessage {...props} />
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
@@ -147,24 +176,35 @@ function MessageComponent({ message }: { message: ChatMessage }): React.JSX.Elem
|
||||
}
|
||||
}
|
||||
|
||||
function ThinkingIndicator(): React.JSX.Element {
|
||||
function ThinkingIndicator({ theme }: { theme: Theme }): React.JSX.Element {
|
||||
const color = getRoleColor("assistant", theme)
|
||||
|
||||
return (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="yellow">Thinking...</Text>
|
||||
<Text color={color}>Thinking...</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function Chat({ messages, isThinking }: ChatProps): React.JSX.Element {
|
||||
export function Chat({
|
||||
messages,
|
||||
isThinking,
|
||||
theme = "dark",
|
||||
showStats = true,
|
||||
showToolCalls = true,
|
||||
}: ChatProps): React.JSX.Element {
|
||||
return (
|
||||
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
||||
{messages.map((message, index) => (
|
||||
<MessageComponent
|
||||
key={`${String(message.timestamp)}-${String(index)}`}
|
||||
message={message}
|
||||
theme={theme}
|
||||
showStats={showStats}
|
||||
showToolCalls={showToolCalls}
|
||||
/>
|
||||
))}
|
||||
{isThinking && <ThinkingIndicator />}
|
||||
{isThinking && <ThinkingIndicator theme={theme} />}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Box, Text } from "ink"
|
||||
import type React from "react"
|
||||
import type { BranchInfo, TuiStatus } from "../types.js"
|
||||
import { getContextColor, getStatusColor, type Theme } from "../utils/theme.js"
|
||||
|
||||
export interface StatusBarProps {
|
||||
contextUsage: number
|
||||
@@ -13,27 +14,30 @@ export interface StatusBarProps {
|
||||
branch: BranchInfo
|
||||
sessionTime: string
|
||||
status: TuiStatus
|
||||
theme?: Theme
|
||||
}
|
||||
|
||||
function getStatusIndicator(status: TuiStatus): { text: string; color: string } {
|
||||
function getStatusIndicator(status: TuiStatus, theme: Theme): { text: string; color: string } {
|
||||
const color = getStatusColor(status, theme)
|
||||
|
||||
switch (status) {
|
||||
case "ready": {
|
||||
return { text: "ready", color: "green" }
|
||||
return { text: "ready", color }
|
||||
}
|
||||
case "thinking": {
|
||||
return { text: "thinking...", color: "yellow" }
|
||||
return { text: "thinking...", color }
|
||||
}
|
||||
case "tool_call": {
|
||||
return { text: "executing...", color: "cyan" }
|
||||
return { text: "executing...", color }
|
||||
}
|
||||
case "awaiting_confirmation": {
|
||||
return { text: "confirm?", color: "magenta" }
|
||||
return { text: "confirm?", color }
|
||||
}
|
||||
case "error": {
|
||||
return { text: "error", color: "red" }
|
||||
return { text: "error", color }
|
||||
}
|
||||
default: {
|
||||
return { text: "ready", color: "green" }
|
||||
return { text: "ready", color }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,9 +52,11 @@ export function StatusBar({
|
||||
branch,
|
||||
sessionTime,
|
||||
status,
|
||||
theme = "dark",
|
||||
}: StatusBarProps): React.JSX.Element {
|
||||
const statusIndicator = getStatusIndicator(status)
|
||||
const statusIndicator = getStatusIndicator(status, theme)
|
||||
const branchDisplay = branch.isDetached ? `HEAD@${branch.name.slice(0, 7)}` : branch.name
|
||||
const contextColor = getContextColor(contextUsage, theme)
|
||||
|
||||
return (
|
||||
<Box borderStyle="single" borderColor="gray" paddingX={1} justifyContent="space-between">
|
||||
@@ -59,11 +65,7 @@ export function StatusBar({
|
||||
[ipuaro]
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
[ctx:{" "}
|
||||
<Text color={contextUsage > 0.8 ? "red" : "white"}>
|
||||
{formatContextUsage(contextUsage)}
|
||||
</Text>
|
||||
]
|
||||
[ctx: <Text color={contextColor}>{formatContextUsage(contextUsage)}</Text>]
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
[<Text color="blue">{projectName}</Text>]
|
||||
|
||||
11
packages/ipuaro/src/tui/utils/bell.ts
Normal file
11
packages/ipuaro/src/tui/utils/bell.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Bell notification utility for terminal.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ring the terminal bell.
|
||||
* Works by outputting the ASCII bell character (\u0007).
|
||||
*/
|
||||
export function ringBell(): void {
|
||||
process.stdout.write("\u0007")
|
||||
}
|
||||
115
packages/ipuaro/src/tui/utils/theme.ts
Normal file
115
packages/ipuaro/src/tui/utils/theme.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Theme color utilities for TUI.
|
||||
*/
|
||||
|
||||
export type Theme = "dark" | "light"
|
||||
|
||||
/**
|
||||
* Color scheme for a theme.
|
||||
*/
|
||||
export interface ColorScheme {
|
||||
primary: string
|
||||
secondary: string
|
||||
success: string
|
||||
warning: string
|
||||
error: string
|
||||
info: string
|
||||
muted: string
|
||||
background: string
|
||||
foreground: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark theme color scheme (default).
|
||||
*/
|
||||
const DARK_THEME: ColorScheme = {
|
||||
primary: "cyan",
|
||||
secondary: "blue",
|
||||
success: "green",
|
||||
warning: "yellow",
|
||||
error: "red",
|
||||
info: "cyan",
|
||||
muted: "gray",
|
||||
background: "black",
|
||||
foreground: "white",
|
||||
}
|
||||
|
||||
/**
|
||||
* Light theme color scheme.
|
||||
*/
|
||||
const LIGHT_THEME: ColorScheme = {
|
||||
primary: "blue",
|
||||
secondary: "cyan",
|
||||
success: "green",
|
||||
warning: "yellow",
|
||||
error: "red",
|
||||
info: "blue",
|
||||
muted: "gray",
|
||||
background: "white",
|
||||
foreground: "black",
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color scheme for a theme.
|
||||
*/
|
||||
export function getColorScheme(theme: Theme): ColorScheme {
|
||||
return theme === "dark" ? DARK_THEME : LIGHT_THEME
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for a status.
|
||||
*/
|
||||
export function getStatusColor(
|
||||
status: "ready" | "thinking" | "error" | "tool_call" | "awaiting_confirmation",
|
||||
theme: Theme = "dark",
|
||||
): string {
|
||||
const scheme = getColorScheme(theme)
|
||||
|
||||
switch (status) {
|
||||
case "ready":
|
||||
return scheme.success
|
||||
case "thinking":
|
||||
case "tool_call":
|
||||
return scheme.warning
|
||||
case "awaiting_confirmation":
|
||||
return scheme.info
|
||||
case "error":
|
||||
return scheme.error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for a message role.
|
||||
*/
|
||||
export function getRoleColor(
|
||||
role: "user" | "assistant" | "system" | "tool",
|
||||
theme: Theme = "dark",
|
||||
): string {
|
||||
const scheme = getColorScheme(theme)
|
||||
|
||||
switch (role) {
|
||||
case "user":
|
||||
return scheme.success
|
||||
case "assistant":
|
||||
return scheme.primary
|
||||
case "system":
|
||||
return scheme.muted
|
||||
case "tool":
|
||||
return scheme.secondary
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for context usage percentage.
|
||||
*/
|
||||
export function getContextColor(usage: number, theme: Theme = "dark"): string {
|
||||
const scheme = getColorScheme(theme)
|
||||
|
||||
if (usage >= 0.8) {
|
||||
return scheme.error
|
||||
}
|
||||
if (usage >= 0.6) {
|
||||
return scheme.warning
|
||||
}
|
||||
return scheme.success
|
||||
}
|
||||
Reference in New Issue
Block a user