mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
Compare commits
7 Commits
ipuaro-v0.
...
ipuaro-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c34d57c231 | ||
|
|
60052c0db9 | ||
|
|
fa647c41aa | ||
|
|
98b365bd94 | ||
|
|
a7669f8947 | ||
|
|
7f0ec49c90 | ||
|
|
077d160343 |
@@ -5,6 +5,252 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.22.4] - 2025-12-02 - Autocomplete Configuration
|
||||
|
||||
### Added
|
||||
|
||||
- **AutocompleteConfigSchema (0.22.4)**
|
||||
- New configuration schema for autocomplete settings in `src/shared/constants/config.ts`
|
||||
- `enabled: boolean` (default: true) - toggle autocomplete feature
|
||||
- `source: "redis-index" | "filesystem" | "both"` (default: "redis-index") - autocomplete source
|
||||
- `maxSuggestions: number` (default: 10) - maximum number of suggestions to display
|
||||
- Integrated into main ConfigSchema with `.default({})`
|
||||
- Exported `AutocompleteConfig` type from config module
|
||||
|
||||
### Changed
|
||||
|
||||
- **useAutocomplete Hook**
|
||||
- Added optional `config?: AutocompleteConfig` parameter to `UseAutocompleteOptions`
|
||||
- Config priority: `config` → `props` → `defaults`
|
||||
- Reads `enabled` and `maxSuggestions` from config if provided
|
||||
- Falls back to prop values, then to defaults
|
||||
- Internal variables renamed: `enabled` → `isEnabled`, `maxSuggestions` → `maxSuggestionsCount`
|
||||
|
||||
- **Chat Component**
|
||||
- Fixed ESLint error: removed unused `roleColor` variable in `ToolMessage` component
|
||||
- Removed unused `theme` parameter from `ToolMessage` function signature
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1657 passed (was 1630, +27 new tests)
|
||||
- New test file: `autocomplete-config.test.ts` with 27 tests
|
||||
- Default values validation (enabled, source, maxSuggestions)
|
||||
- `enabled` boolean validation
|
||||
- `source` enum validation ("redis-index", "filesystem", "both")
|
||||
- `maxSuggestions` positive integer validation (including edge cases: zero, negative, float rejection)
|
||||
- Partial and full config merging tests
|
||||
- Coverage: 97.59% lines, 91.23% branches, 98.77% functions, 97.59% statements
|
||||
- 0 ESLint errors, 5 warnings (acceptable TUI component warnings)
|
||||
- Build successful with no TypeScript errors
|
||||
|
||||
### Notes
|
||||
|
||||
This release completes the fourth item (0.22.4) of the v0.22.0 Extended Configuration milestone. Remaining item for v0.22.0:
|
||||
- 0.22.5 - Commands Configuration
|
||||
|
||||
---
|
||||
|
||||
## [0.22.3] - 2025-12-02 - Context Configuration
|
||||
|
||||
### Added
|
||||
|
||||
- **ContextConfigSchema (0.22.3)**
|
||||
- New configuration schema for context management in `src/shared/constants/config.ts`
|
||||
- `systemPromptTokens: number` (default: 2000) - token budget for system prompt
|
||||
- `maxContextUsage: number` (default: 0.8) - maximum context window usage ratio (0-1)
|
||||
- `autoCompressAt: number` (default: 0.8) - threshold for automatic context compression (0-1)
|
||||
- `compressionMethod: "llm-summary" | "truncate"` (default: "llm-summary") - compression strategy
|
||||
- Integrated into main ConfigSchema with `.default({})`
|
||||
- Exported `ContextConfig` type from config module
|
||||
|
||||
### Changed
|
||||
|
||||
- **ContextManager**
|
||||
- Added optional `config?: ContextConfig` parameter to constructor
|
||||
- Added private `compressionThreshold: number` field (read from config or default)
|
||||
- Added private `compressionMethod: "llm-summary" | "truncate"` field (read from config or default)
|
||||
- Updated `needsCompression()` to use configurable `compressionThreshold` instead of hardcoded constant
|
||||
- Enables dynamic compression threshold configuration per session/deployment
|
||||
|
||||
- **HandleMessage Use Case**
|
||||
- Added optional `contextConfig?: ContextConfig` parameter to constructor
|
||||
- Added `contextConfig?: ContextConfig` to `HandleMessageOptions`
|
||||
- Passes context config to ContextManager during initialization
|
||||
- Context management behavior now fully configurable
|
||||
|
||||
- **useSession Hook**
|
||||
- Passes `deps.config?.context` to HandleMessage constructor
|
||||
- Passes `contextConfig: deps.config?.context` to HandleMessage options
|
||||
- Context configuration flows from config through to ContextManager
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1630 passed (was 1590, +40 new tests)
|
||||
- New test file: `context-config.test.ts` with 32 tests
|
||||
- Default values validation (systemPromptTokens, maxContextUsage, autoCompressAt, compressionMethod)
|
||||
- `systemPromptTokens` positive integer validation (including edge cases: zero, negative, float rejection)
|
||||
- `maxContextUsage` ratio validation (0-1 range, rejects out-of-bounds)
|
||||
- `autoCompressAt` ratio validation (0-1 range, rejects out-of-bounds)
|
||||
- `compressionMethod` enum validation (llm-summary, truncate)
|
||||
- Partial and full config merging tests
|
||||
- Updated ContextManager tests: 8 new tests for configuration integration
|
||||
- Custom compression threshold behavior
|
||||
- Edge cases: autoCompressAt = 0 and autoCompressAt = 1
|
||||
- Full context config acceptance
|
||||
- Coverage: 97.63% lines, 91.34% branches, 98.77% functions, 97.63% statements
|
||||
- 0 ESLint errors, 0 warnings
|
||||
- Build successful with no TypeScript errors
|
||||
|
||||
### Notes
|
||||
|
||||
This release completes the third item (0.22.3) of the v0.22.0 Extended Configuration milestone. Remaining items for v0.22.0:
|
||||
- 0.22.4 - Autocomplete Configuration
|
||||
- 0.22.5 - Commands Configuration
|
||||
|
||||
---
|
||||
|
||||
## [0.22.2] - 2025-12-02 - Session Configuration
|
||||
|
||||
### Added
|
||||
|
||||
- **SessionConfigSchema (0.22.2)**
|
||||
- New configuration schema for session settings in `src/shared/constants/config.ts`
|
||||
- `persistIndefinitely: boolean` (default: true) - toggle indefinite session persistence
|
||||
- `maxHistoryMessages: number` (default: 100) - maximum number of messages to keep in session history
|
||||
- `saveInputHistory: boolean` (default: true) - toggle saving user input to history
|
||||
- Integrated into main ConfigSchema with `.default({})`
|
||||
- Exported `SessionConfig` type from config module
|
||||
|
||||
- **Session.truncateHistory() Method**
|
||||
- New method in `src/domain/entities/Session.ts`
|
||||
- Truncates message history to specified maximum length
|
||||
- Keeps most recent messages when truncating
|
||||
|
||||
### Changed
|
||||
|
||||
- **HandleMessage Use Case**
|
||||
- Added `maxHistoryMessages?: number` option to `HandleMessageOptions`
|
||||
- Added `saveInputHistory?: boolean` option to `HandleMessageOptions`
|
||||
- Added `truncateHistoryIfNeeded()` private method for automatic history truncation
|
||||
- Calls `truncateHistoryIfNeeded()` after every message addition (6 locations)
|
||||
- Checks `saveInputHistory` before saving input to history
|
||||
- Ensures history stays within configured limits automatically
|
||||
|
||||
- **useSession Hook**
|
||||
- Added `config?: Config` to `UseSessionDependencies`
|
||||
- Passes `maxHistoryMessages` and `saveInputHistory` from config to HandleMessage options
|
||||
- Session configuration now flows from config through to message handling
|
||||
|
||||
- **App Component**
|
||||
- Added `config?: Config` to `AppDependencies`
|
||||
- Passes config to useSession hook
|
||||
- Enables configuration-driven session management
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1590 passed (was 1571, +19 new tests)
|
||||
- New test file: `session-config.test.ts` with 19 tests
|
||||
- Default values validation
|
||||
- `persistIndefinitely` boolean validation
|
||||
- `maxHistoryMessages` positive integer validation (including edge cases: zero, negative, float rejection)
|
||||
- `saveInputHistory` boolean validation
|
||||
- Partial and full config merging tests
|
||||
- Coverage: 97.62% lines, 91.32% branches, 98.77% functions, 97.62% statements
|
||||
- 0 ESLint errors, 0 warnings
|
||||
- Build successful with no TypeScript errors
|
||||
|
||||
### Notes
|
||||
|
||||
This release completes the second item (0.22.2) of the v0.22.0 Extended Configuration milestone. Remaining items for v0.22.0:
|
||||
- 0.22.3 - Context Configuration
|
||||
- 0.22.4 - Autocomplete Configuration
|
||||
- 0.22.5 - Commands Configuration
|
||||
|
||||
---
|
||||
|
||||
## [0.22.1] - 2025-12-02 - Display Configuration
|
||||
|
||||
### Added
|
||||
|
||||
- **DisplayConfigSchema (0.22.1)**
|
||||
- New configuration schema for display settings in `src/shared/constants/config.ts`
|
||||
- `showStats: boolean` (default: true) - toggle statistics display in chat
|
||||
- `showToolCalls: boolean` (default: true) - toggle tool calls display in chat
|
||||
- `theme: "dark" | "light"` (default: "dark") - color theme for TUI
|
||||
- `bellOnComplete: boolean` (default: false) - ring terminal bell on completion
|
||||
- `progressBar: boolean` (default: true) - toggle progress bar display
|
||||
- Integrated into main ConfigSchema with `.default({})`
|
||||
- Exported `DisplayConfig` type from config module
|
||||
|
||||
- **Theme Utilities (0.22.1)**
|
||||
- New `theme.ts` utility in `src/tui/utils/theme.ts`
|
||||
- `Theme` type: "dark" | "light"
|
||||
- `ColorScheme` interface with semantic colors (primary, secondary, success, warning, error, info, muted)
|
||||
- Dark theme colors: cyan primary, blue secondary, black background, white foreground
|
||||
- Light theme colors: blue primary, cyan secondary, white background, black foreground
|
||||
- `getColorScheme()` - get color scheme for theme
|
||||
- `getStatusColor()` - dynamic colors for status (ready, thinking, error, tool_call, awaiting_confirmation)
|
||||
- `getRoleColor()` - dynamic colors for message roles (user, assistant, system, tool)
|
||||
- `getContextColor()` - dynamic colors for context usage (green <60%, yellow 60-79%, red ≥80%)
|
||||
|
||||
- **Bell Notification (0.22.1)**
|
||||
- New `bell.ts` utility in `src/tui/utils/bell.ts`
|
||||
- `ringBell()` function for terminal bell notification
|
||||
- Uses ASCII bell character (\u0007) via stdout
|
||||
- Triggered when status changes to "ready" if `bellOnComplete` enabled
|
||||
|
||||
### Changed
|
||||
|
||||
- **StatusBar Component**
|
||||
- Added `theme?: Theme` prop (default: "dark")
|
||||
- Uses `getStatusColor()` for dynamic status indicator colors
|
||||
- Uses `getContextColor()` for dynamic context usage colors
|
||||
- Theme-aware color scheme throughout component
|
||||
|
||||
- **Chat Component**
|
||||
- Added `theme?: Theme` prop (default: "dark")
|
||||
- Added `showStats?: boolean` prop (default: true)
|
||||
- Added `showToolCalls?: boolean` prop (default: true)
|
||||
- Created `MessageComponentProps` interface for consistent prop passing
|
||||
- All message subcomponents (UserMessage, AssistantMessage, ToolMessage, SystemMessage) now theme-aware
|
||||
- Uses `getRoleColor()` for dynamic message role colors
|
||||
- Stats conditionally displayed based on `showStats`
|
||||
- Tool calls conditionally displayed based on `showToolCalls`
|
||||
- ThinkingIndicator now theme-aware
|
||||
|
||||
- **App Component**
|
||||
- Added `theme?: "dark" | "light"` prop (default: "dark")
|
||||
- Added `showStats?: boolean` prop (default: true)
|
||||
- Added `showToolCalls?: boolean` prop (default: true)
|
||||
- Added `bellOnComplete?: boolean` prop (default: false)
|
||||
- Extended `ExtendedAppProps` interface with display config props
|
||||
- Passes display config to StatusBar and Chat components
|
||||
- Added useEffect hook for bell notification on status change to "ready"
|
||||
- Imports `ringBell` utility
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1571 (was 1525, +46 new tests)
|
||||
- New test files:
|
||||
- `display-config.test.ts` with 20 tests (schema validation)
|
||||
- `theme.test.ts` with 24 tests (color scheme, status/role/context colors)
|
||||
- `bell.test.ts` with 2 tests (stdout write verification)
|
||||
- Coverage: 97.68% lines, 91.38% branches, 98.97% functions, 97.68% statements
|
||||
- 0 ESLint errors, 0 warnings
|
||||
- Build successful with no TypeScript errors
|
||||
- 3 new utility files created, 4 components updated
|
||||
- All display options configurable via DisplayConfigSchema
|
||||
|
||||
### Notes
|
||||
|
||||
This release completes the first item (0.22.1) of the v0.22.0 Extended Configuration milestone. Remaining items for v0.22.0:
|
||||
- 0.22.2 - Session Configuration
|
||||
- 0.22.3 - Context Configuration
|
||||
- 0.22.4 - Autocomplete Configuration
|
||||
- 0.22.5 - Commands Configuration
|
||||
|
||||
---
|
||||
|
||||
## [0.21.4] - 2025-12-02 - Syntax Highlighting in DiffView
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1648,9 +1648,9 @@ interface DiffViewProps {
|
||||
## Version 0.22.0 - Extended Configuration ⚙️
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** Pending
|
||||
**Status:** In Progress (4/5 complete)
|
||||
|
||||
### 0.22.1 - Display Configuration
|
||||
### 0.22.1 - Display Configuration ✅
|
||||
|
||||
```typescript
|
||||
// src/shared/constants/config.ts additions
|
||||
@@ -1664,13 +1664,13 @@ export const DisplayConfigSchema = z.object({
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] DisplayConfigSchema in config.ts
|
||||
- [ ] Bell notification on response complete
|
||||
- [ ] Theme support (dark/light color schemes)
|
||||
- [ ] Configurable stats display
|
||||
- [ ] Unit tests
|
||||
- [x] DisplayConfigSchema in config.ts
|
||||
- [x] Bell notification on response complete
|
||||
- [x] Theme support (dark/light color schemes)
|
||||
- [x] Configurable stats display
|
||||
- [x] Unit tests (46 new tests: 20 schema, 24 theme, 2 bell)
|
||||
|
||||
### 0.22.2 - Session Configuration
|
||||
### 0.22.2 - Session Configuration ✅
|
||||
|
||||
```typescript
|
||||
// src/shared/constants/config.ts additions
|
||||
@@ -1682,12 +1682,12 @@ export const SessionConfigSchema = z.object({
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] SessionConfigSchema in config.ts
|
||||
- [ ] History truncation based on maxHistoryMessages
|
||||
- [ ] Input history persistence toggle
|
||||
- [ ] Unit tests
|
||||
- [x] SessionConfigSchema in config.ts
|
||||
- [x] History truncation based on maxHistoryMessages
|
||||
- [x] Input history persistence toggle
|
||||
- [x] Unit tests (19 new tests)
|
||||
|
||||
### 0.22.3 - Context Configuration
|
||||
### 0.22.3 - Context Configuration ✅
|
||||
|
||||
```typescript
|
||||
// src/shared/constants/config.ts additions
|
||||
@@ -1700,12 +1700,12 @@ export const ContextConfigSchema = z.object({
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] ContextConfigSchema in config.ts
|
||||
- [ ] ContextManager reads from config
|
||||
- [ ] Configurable compression threshold
|
||||
- [ ] Unit tests
|
||||
- [x] ContextConfigSchema in config.ts
|
||||
- [x] ContextManager reads from config
|
||||
- [x] Configurable compression threshold
|
||||
- [x] Unit tests (40 new tests: 32 schema, 8 ContextManager integration)
|
||||
|
||||
### 0.22.4 - Autocomplete Configuration
|
||||
### 0.22.4 - Autocomplete Configuration ✅
|
||||
|
||||
```typescript
|
||||
// src/shared/constants/config.ts additions
|
||||
@@ -1717,9 +1717,9 @@ export const AutocompleteConfigSchema = z.object({
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] AutocompleteConfigSchema in config.ts
|
||||
- [ ] useAutocomplete reads from config
|
||||
- [ ] Unit tests
|
||||
- [x] AutocompleteConfigSchema in config.ts
|
||||
- [x] useAutocomplete reads from config
|
||||
- [x] Unit tests (27 tests)
|
||||
|
||||
### 0.22.5 - Commands Configuration
|
||||
|
||||
@@ -1880,6 +1880,6 @@ sessions:list # List<session_id>
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-12-01
|
||||
**Last Updated:** 2025-12-02
|
||||
**Target Version:** 1.0.0
|
||||
**Current Version:** 0.18.0
|
||||
**Current Version:** 0.22.1
|
||||
@@ -79,7 +79,7 @@ export class AuthService {
|
||||
return {
|
||||
token,
|
||||
expiresAt,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ async function main(): Promise<void> {
|
||||
email: "demo@example.com",
|
||||
name: "Demo User",
|
||||
password: "password123",
|
||||
role: "admin"
|
||||
role: "admin",
|
||||
})
|
||||
|
||||
logger.info("Demo user created", { userId: user.id })
|
||||
|
||||
@@ -25,9 +25,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = Array.from(this.users.values()).find(
|
||||
(u) => u.email === dto.email
|
||||
)
|
||||
const existingUser = Array.from(this.users.values()).find((u) => u.email === dto.email)
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error("User with this email already exists")
|
||||
@@ -40,7 +38,7 @@ export class UserService {
|
||||
name: dto.name,
|
||||
role: dto.role || "user",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
this.users.set(user.id, user)
|
||||
@@ -71,7 +69,7 @@ export class UserService {
|
||||
...user,
|
||||
...(dto.name && { name: dto.name }),
|
||||
...(dto.role && { role: dto.role }),
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
this.users.set(id, updated)
|
||||
|
||||
@@ -30,7 +30,7 @@ export class Logger {
|
||||
level,
|
||||
context: this.context,
|
||||
message,
|
||||
...(meta && { meta })
|
||||
...(meta && { meta }),
|
||||
}
|
||||
console.log(JSON.stringify(logEntry))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function sanitizeInput(input: string): string {
|
||||
export class ValidationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public field: string
|
||||
public field: string,
|
||||
) {
|
||||
super(message)
|
||||
this.name = "ValidationError"
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("UserService", () => {
|
||||
const user = await userService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
password: "password123"
|
||||
password: "password123",
|
||||
})
|
||||
|
||||
expect(user).toBeDefined()
|
||||
@@ -32,8 +32,8 @@ describe("UserService", () => {
|
||||
userService.createUser({
|
||||
email: "invalid-email",
|
||||
name: "Test User",
|
||||
password: "password123"
|
||||
})
|
||||
password: "password123",
|
||||
}),
|
||||
).rejects.toThrow(ValidationError)
|
||||
})
|
||||
|
||||
@@ -42,8 +42,8 @@ describe("UserService", () => {
|
||||
userService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
password: "weak"
|
||||
})
|
||||
password: "weak",
|
||||
}),
|
||||
).rejects.toThrow(ValidationError)
|
||||
})
|
||||
|
||||
@@ -51,15 +51,15 @@ describe("UserService", () => {
|
||||
await userService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
password: "password123"
|
||||
password: "password123",
|
||||
})
|
||||
|
||||
await expect(
|
||||
userService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Another User",
|
||||
password: "password123"
|
||||
})
|
||||
password: "password123",
|
||||
}),
|
||||
).rejects.toThrow("already exists")
|
||||
})
|
||||
})
|
||||
@@ -69,7 +69,7 @@ describe("UserService", () => {
|
||||
const created = await userService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
password: "password123"
|
||||
password: "password123",
|
||||
})
|
||||
|
||||
const found = await userService.getUserById(created.id)
|
||||
@@ -87,11 +87,11 @@ describe("UserService", () => {
|
||||
const user = await userService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
password: "password123"
|
||||
password: "password123",
|
||||
})
|
||||
|
||||
const updated = await userService.updateUser(user.id, {
|
||||
name: "Updated Name"
|
||||
name: "Updated Name",
|
||||
})
|
||||
|
||||
expect(updated.name).toBe("Updated Name")
|
||||
@@ -99,9 +99,9 @@ describe("UserService", () => {
|
||||
})
|
||||
|
||||
it("should throw error for non-existent user", async () => {
|
||||
await expect(
|
||||
userService.updateUser("non-existent", { name: "Test" })
|
||||
).rejects.toThrow("not found")
|
||||
await expect(userService.updateUser("non-existent", { name: "Test" })).rejects.toThrow(
|
||||
"not found",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -110,7 +110,7 @@ describe("UserService", () => {
|
||||
const user = await userService.createUser({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
password: "password123"
|
||||
password: "password123",
|
||||
})
|
||||
|
||||
await userService.deleteUser(user.id)
|
||||
@@ -125,13 +125,13 @@ describe("UserService", () => {
|
||||
await userService.createUser({
|
||||
email: "user1@example.com",
|
||||
name: "User 1",
|
||||
password: "password123"
|
||||
password: "password123",
|
||||
})
|
||||
|
||||
await userService.createUser({
|
||||
email: "user2@example.com",
|
||||
name: "User 2",
|
||||
password: "password123"
|
||||
password: "password123",
|
||||
})
|
||||
|
||||
const users = await userService.listUsers()
|
||||
|
||||
@@ -3,6 +3,6 @@ import { defineConfig } from "vitest/config"
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node"
|
||||
}
|
||||
environment: "node",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@samiyev/ipuaro",
|
||||
"version": "0.21.4",
|
||||
"version": "0.22.4",
|
||||
"description": "Local AI agent for codebase operations with infinite context feeling",
|
||||
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ContextState, Session } from "../../domain/entities/Session.js"
|
||||
import type { ILLMClient } from "../../domain/services/ILLMClient.js"
|
||||
import { type ChatMessage, createSystemMessage } from "../../domain/value-objects/ChatMessage.js"
|
||||
import { CONTEXT_COMPRESSION_THRESHOLD, CONTEXT_WINDOW_SIZE } from "../../domain/constants/index.js"
|
||||
import type { ContextConfig } from "../../shared/constants/config.js"
|
||||
|
||||
/**
|
||||
* File in context with token count.
|
||||
@@ -39,9 +40,13 @@ export class ContextManager {
|
||||
private readonly filesInContext = new Map<string, FileContext>()
|
||||
private currentTokens = 0
|
||||
private readonly contextWindowSize: number
|
||||
private readonly compressionThreshold: number
|
||||
private readonly compressionMethod: "llm-summary" | "truncate"
|
||||
|
||||
constructor(contextWindowSize: number = CONTEXT_WINDOW_SIZE) {
|
||||
constructor(contextWindowSize: number = CONTEXT_WINDOW_SIZE, config?: ContextConfig) {
|
||||
this.contextWindowSize = contextWindowSize
|
||||
this.compressionThreshold = config?.autoCompressAt ?? CONTEXT_COMPRESSION_THRESHOLD
|
||||
this.compressionMethod = config?.compressionMethod ?? "llm-summary"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +102,7 @@ export class ContextManager {
|
||||
* Check if compression is needed.
|
||||
*/
|
||||
needsCompression(): boolean {
|
||||
return this.getUsage() > CONTEXT_COMPRESSION_THRESHOLD
|
||||
return this.getUsage() > this.compressionThreshold
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,6 +68,9 @@ export interface HandleMessageEvents {
|
||||
export interface HandleMessageOptions {
|
||||
autoApply?: boolean
|
||||
maxToolCalls?: number
|
||||
maxHistoryMessages?: number
|
||||
saveInputHistory?: boolean
|
||||
contextConfig?: import("../../shared/constants/config.js").ContextConfig
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_TOOL_CALLS = 20
|
||||
@@ -96,13 +99,14 @@ export class HandleMessage {
|
||||
llm: ILLMClient,
|
||||
tools: IToolRegistry,
|
||||
projectRoot: string,
|
||||
contextConfig?: import("../../shared/constants/config.js").ContextConfig,
|
||||
) {
|
||||
this.storage = storage
|
||||
this.sessionStorage = sessionStorage
|
||||
this.llm = llm
|
||||
this.tools = tools
|
||||
this.projectRoot = projectRoot
|
||||
this.contextManager = new ContextManager(llm.getContextWindowSize())
|
||||
this.contextManager = new ContextManager(llm.getContextWindowSize(), contextConfig)
|
||||
this.executeTool = new ExecuteTool(storage, sessionStorage, tools, projectRoot)
|
||||
}
|
||||
|
||||
@@ -135,6 +139,15 @@ export class HandleMessage {
|
||||
this.llm.abort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate session history if maxHistoryMessages is set.
|
||||
*/
|
||||
private truncateHistoryIfNeeded(session: Session): void {
|
||||
if (this.options.maxHistoryMessages !== undefined) {
|
||||
session.truncateHistory(this.options.maxHistoryMessages)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the message handling flow.
|
||||
*/
|
||||
@@ -145,7 +158,12 @@ export class HandleMessage {
|
||||
if (message.trim()) {
|
||||
const userMessage = createUserMessage(message)
|
||||
session.addMessage(userMessage)
|
||||
session.addInputToHistory(message)
|
||||
this.truncateHistoryIfNeeded(session)
|
||||
|
||||
if (this.options.saveInputHistory !== false) {
|
||||
session.addInputToHistory(message)
|
||||
}
|
||||
|
||||
this.emitMessage(userMessage)
|
||||
}
|
||||
|
||||
@@ -183,6 +201,7 @@ export class HandleMessage {
|
||||
toolCalls: 0,
|
||||
})
|
||||
session.addMessage(assistantMessage)
|
||||
this.truncateHistoryIfNeeded(session)
|
||||
this.emitMessage(assistantMessage)
|
||||
this.contextManager.addTokens(response.tokens)
|
||||
this.contextManager.updateSession(session)
|
||||
@@ -197,6 +216,7 @@ export class HandleMessage {
|
||||
toolCalls: parsed.toolCalls.length,
|
||||
})
|
||||
session.addMessage(assistantMessage)
|
||||
this.truncateHistoryIfNeeded(session)
|
||||
this.emitMessage(assistantMessage)
|
||||
|
||||
toolCallCount += parsed.toolCalls.length
|
||||
@@ -204,6 +224,7 @@ export class HandleMessage {
|
||||
const errorMsg = `Maximum tool calls (${String(maxToolCalls)}) exceeded`
|
||||
const errorMessage = createSystemMessage(errorMsg)
|
||||
session.addMessage(errorMessage)
|
||||
this.truncateHistoryIfNeeded(session)
|
||||
this.emitMessage(errorMessage)
|
||||
this.emitStatus("ready")
|
||||
return
|
||||
@@ -227,6 +248,7 @@ export class HandleMessage {
|
||||
|
||||
const toolMessage = createToolMessage(results)
|
||||
session.addMessage(toolMessage)
|
||||
this.truncateHistoryIfNeeded(session)
|
||||
|
||||
this.contextManager.addTokens(response.tokens)
|
||||
|
||||
@@ -306,6 +328,7 @@ export class HandleMessage {
|
||||
|
||||
const errorMessage = createSystemMessage(`Error: ${ipuaroError.message}`)
|
||||
session.addMessage(errorMessage)
|
||||
this.truncateHistoryIfNeeded(session)
|
||||
this.emitMessage(errorMessage)
|
||||
|
||||
this.emitStatus("ready")
|
||||
|
||||
@@ -94,6 +94,12 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
truncateHistory(maxMessages: number): void {
|
||||
if (this.history.length > maxMessages) {
|
||||
this.history = this.history.slice(-maxMessages)
|
||||
}
|
||||
}
|
||||
|
||||
clearHistory(): void {
|
||||
this.history = []
|
||||
this.context = {
|
||||
|
||||
@@ -86,6 +86,45 @@ 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),
|
||||
})
|
||||
|
||||
/**
|
||||
* Session configuration schema.
|
||||
*/
|
||||
export const SessionConfigSchema = z.object({
|
||||
persistIndefinitely: z.boolean().default(true),
|
||||
maxHistoryMessages: z.number().int().positive().default(100),
|
||||
saveInputHistory: z.boolean().default(true),
|
||||
})
|
||||
|
||||
/**
|
||||
* Context configuration schema.
|
||||
*/
|
||||
export const ContextConfigSchema = z.object({
|
||||
systemPromptTokens: z.number().int().positive().default(2000),
|
||||
maxContextUsage: z.number().min(0).max(1).default(0.8),
|
||||
autoCompressAt: z.number().min(0).max(1).default(0.8),
|
||||
compressionMethod: z.enum(["llm-summary", "truncate"]).default("llm-summary"),
|
||||
})
|
||||
|
||||
/**
|
||||
* Autocomplete configuration schema.
|
||||
*/
|
||||
export const AutocompleteConfigSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
source: z.enum(["redis-index", "filesystem", "both"]).default("redis-index"),
|
||||
maxSuggestions: z.number().int().positive().default(10),
|
||||
})
|
||||
|
||||
/**
|
||||
* Full configuration schema.
|
||||
*/
|
||||
@@ -97,6 +136,10 @@ export const ConfigSchema = z.object({
|
||||
undo: UndoConfigSchema.default({}),
|
||||
edit: EditConfigSchema.default({}),
|
||||
input: InputConfigSchema.default({}),
|
||||
display: DisplayConfigSchema.default({}),
|
||||
session: SessionConfigSchema.default({}),
|
||||
context: ContextConfigSchema.default({}),
|
||||
autocomplete: AutocompleteConfigSchema.default({}),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -110,6 +153,10 @@ 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>
|
||||
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
||||
export type ContextConfig = z.infer<typeof ContextConfigSchema>
|
||||
export type AutocompleteConfig = z.infer<typeof AutocompleteConfigSchema>
|
||||
|
||||
/**
|
||||
* Default configuration.
|
||||
|
||||
@@ -10,6 +10,7 @@ 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"
|
||||
@@ -17,6 +18,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
|
||||
@@ -24,6 +26,7 @@ export interface AppDependencies {
|
||||
llm: ILLMClient
|
||||
tools: IToolRegistry
|
||||
projectStructure?: ProjectStructure
|
||||
config?: Config
|
||||
}
|
||||
|
||||
export interface ExtendedAppProps extends AppProps {
|
||||
@@ -31,6 +34,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 +76,10 @@ export function App({
|
||||
onExit,
|
||||
multiline = false,
|
||||
syntaxHighlight = true,
|
||||
theme = "dark",
|
||||
showStats = true,
|
||||
showToolCalls = true,
|
||||
bellOnComplete = false,
|
||||
}: ExtendedAppProps): React.JSX.Element {
|
||||
const { exit } = useApp()
|
||||
|
||||
@@ -120,6 +131,7 @@ export function App({
|
||||
projectRoot: projectPath,
|
||||
projectName,
|
||||
projectStructure: deps.projectStructure,
|
||||
config: deps.config,
|
||||
},
|
||||
{
|
||||
autoApply,
|
||||
@@ -193,6 +205,12 @@ export function App({
|
||||
}
|
||||
}, [session])
|
||||
|
||||
useEffect(() => {
|
||||
if (bellOnComplete && status === "ready") {
|
||||
ringBell()
|
||||
}
|
||||
}, [bellOnComplete, status])
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(text: string): void => {
|
||||
if (isCommand(text)) {
|
||||
@@ -228,8 +246,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,7 @@ function AssistantMessage({ message }: { message: ChatMessage }): React.JSX.Elem
|
||||
)
|
||||
}
|
||||
|
||||
function ToolMessage({ message }: { message: ChatMessage }): React.JSX.Element {
|
||||
function ToolMessage({ message }: MessageComponentProps): React.JSX.Element {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
||||
{message.toolResults?.map((result) => (
|
||||
@@ -115,31 +134,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 +174,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>]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import type { IStorage } from "../../domain/services/IStorage.js"
|
||||
import type { AutocompleteConfig } from "../../shared/constants/config.js"
|
||||
import path from "node:path"
|
||||
|
||||
export interface UseAutocompleteOptions {
|
||||
@@ -12,6 +13,7 @@ export interface UseAutocompleteOptions {
|
||||
projectRoot: string
|
||||
enabled?: boolean
|
||||
maxSuggestions?: number
|
||||
config?: AutocompleteConfig
|
||||
}
|
||||
|
||||
export interface UseAutocompleteReturn {
|
||||
@@ -107,13 +109,18 @@ function getCommonPrefix(suggestions: string[]): string {
|
||||
}
|
||||
|
||||
export function useAutocomplete(options: UseAutocompleteOptions): UseAutocompleteReturn {
|
||||
const { storage, projectRoot, enabled = true, maxSuggestions = 10 } = options
|
||||
const { storage, projectRoot, enabled, maxSuggestions, config } = options
|
||||
|
||||
// Read from config if provided, otherwise use options, otherwise use defaults
|
||||
const isEnabled = config?.enabled ?? enabled ?? true
|
||||
const maxSuggestionsCount = config?.maxSuggestions ?? maxSuggestions ?? 10
|
||||
|
||||
const [filePaths, setFilePaths] = useState<string[]>([])
|
||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||
|
||||
// Load file paths from storage
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
if (!isEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,11 +142,11 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
|
||||
loadPaths().catch(() => {
|
||||
// Ignore errors
|
||||
})
|
||||
}, [storage, projectRoot, enabled])
|
||||
}, [storage, projectRoot, isEnabled])
|
||||
|
||||
const complete = useCallback(
|
||||
(partial: string): string[] => {
|
||||
if (!enabled || !partial.trim()) {
|
||||
if (!isEnabled || !partial.trim()) {
|
||||
setSuggestions([])
|
||||
return []
|
||||
}
|
||||
@@ -154,13 +161,13 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
|
||||
}))
|
||||
.filter((item) => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, maxSuggestions)
|
||||
.slice(0, maxSuggestionsCount)
|
||||
.map((item) => item.path)
|
||||
|
||||
setSuggestions(scored)
|
||||
return scored
|
||||
},
|
||||
[enabled, filePaths, maxSuggestions],
|
||||
[isEnabled, filePaths, maxSuggestionsCount],
|
||||
)
|
||||
|
||||
const accept = useCallback(
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { IStorage } from "../../domain/services/IStorage.js"
|
||||
import type { DiffInfo } from "../../domain/services/ITool.js"
|
||||
import type { ChatMessage } from "../../domain/value-objects/ChatMessage.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 {
|
||||
HandleMessage,
|
||||
@@ -30,6 +31,7 @@ export interface UseSessionDependencies {
|
||||
projectRoot: string
|
||||
projectName: string
|
||||
projectStructure?: ProjectStructure
|
||||
config?: Config
|
||||
}
|
||||
|
||||
export interface UseSessionOptions {
|
||||
@@ -107,11 +109,17 @@ async function initializeSession(
|
||||
deps.llm,
|
||||
deps.tools,
|
||||
deps.projectRoot,
|
||||
deps.config?.context,
|
||||
)
|
||||
if (deps.projectStructure) {
|
||||
handleMessage.setProjectStructure(deps.projectStructure)
|
||||
}
|
||||
handleMessage.setOptions({ autoApply: options.autoApply })
|
||||
handleMessage.setOptions({
|
||||
autoApply: options.autoApply,
|
||||
maxHistoryMessages: deps.config?.session.maxHistoryMessages,
|
||||
saveInputHistory: deps.config?.session.saveInputHistory,
|
||||
contextConfig: deps.config?.context,
|
||||
})
|
||||
handleMessage.setEvents(createEventHandlers(setters, options))
|
||||
refs.current.handleMessage = handleMessage
|
||||
refs.current.undoChange = new UndoChange(deps.sessionStorage, deps.storage)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -245,4 +245,65 @@ describe("ContextManager", () => {
|
||||
expect(state.needsCompression).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("configuration", () => {
|
||||
it("should use default compression threshold when no config provided", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE)
|
||||
manager.addToContext("test.ts", CONTEXT_SIZE * 0.85)
|
||||
|
||||
expect(manager.needsCompression()).toBe(true)
|
||||
})
|
||||
|
||||
it("should use custom compression threshold from config", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 0.9 })
|
||||
manager.addToContext("test.ts", CONTEXT_SIZE * 0.85)
|
||||
|
||||
expect(manager.needsCompression()).toBe(false)
|
||||
})
|
||||
|
||||
it("should trigger compression at custom threshold", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 0.9 })
|
||||
manager.addToContext("test.ts", CONTEXT_SIZE * 0.95)
|
||||
|
||||
expect(manager.needsCompression()).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept compression method in config", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE, { compressionMethod: "truncate" })
|
||||
|
||||
expect(manager).toBeDefined()
|
||||
})
|
||||
|
||||
it("should use default compression method when not specified", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE, {})
|
||||
|
||||
expect(manager).toBeDefined()
|
||||
})
|
||||
|
||||
it("should accept full context config", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE, {
|
||||
systemPromptTokens: 3000,
|
||||
maxContextUsage: 0.9,
|
||||
autoCompressAt: 0.85,
|
||||
compressionMethod: "llm-summary",
|
||||
})
|
||||
|
||||
manager.addToContext("test.ts", CONTEXT_SIZE * 0.87)
|
||||
expect(manager.needsCompression()).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle edge case: autoCompressAt = 0", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 0 })
|
||||
manager.addToContext("test.ts", 1)
|
||||
|
||||
expect(manager.needsCompression()).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle edge case: autoCompressAt = 1", () => {
|
||||
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 1 })
|
||||
manager.addToContext("test.ts", CONTEXT_SIZE * 0.99)
|
||||
|
||||
expect(manager.needsCompression()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@ vi.mock("../../../../src/infrastructure/indexer/FileScanner.js", () => ({
|
||||
return 'export function main() { return "hello" }'
|
||||
}
|
||||
if (path.includes("utils.ts")) {
|
||||
return 'export const add = (a: number, b: number) => a + b'
|
||||
return "export const add = (a: number, b: number) => a + b"
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -31,7 +31,16 @@ vi.mock("../../../../src/infrastructure/indexer/ASTParser.js", () => ({
|
||||
parse() {
|
||||
return {
|
||||
...createEmptyFileAST(),
|
||||
functions: [{ name: "test", lineStart: 1, lineEnd: 5, params: [], isAsync: false, isExported: true }],
|
||||
functions: [
|
||||
{
|
||||
name: "test",
|
||||
lineStart: 1,
|
||||
lineEnd: 5,
|
||||
params: [],
|
||||
isAsync: false,
|
||||
isExported: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -116,7 +125,7 @@ describe("IndexProject", () => {
|
||||
expect.objectContaining({
|
||||
hash: expect.any(String),
|
||||
lines: expect.any(Array),
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -128,7 +137,7 @@ describe("IndexProject", () => {
|
||||
"src/index.ts",
|
||||
expect.objectContaining({
|
||||
functions: expect.any(Array),
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -136,19 +145,14 @@ describe("IndexProject", () => {
|
||||
await useCase.execute("/test/project")
|
||||
|
||||
expect(mockStorage.setMeta).toHaveBeenCalledTimes(2)
|
||||
expect(mockStorage.setMeta).toHaveBeenCalledWith(
|
||||
"src/index.ts",
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(mockStorage.setMeta).toHaveBeenCalledWith("src/index.ts", expect.any(Object))
|
||||
})
|
||||
|
||||
it("should build and store symbol index", async () => {
|
||||
await useCase.execute("/test/project")
|
||||
|
||||
expect(mockStorage.setSymbolIndex).toHaveBeenCalledTimes(1)
|
||||
expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith(
|
||||
expect.any(Map)
|
||||
)
|
||||
expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith(expect.any(Map))
|
||||
})
|
||||
|
||||
it("should build and store dependency graph", async () => {
|
||||
@@ -159,7 +163,7 @@ describe("IndexProject", () => {
|
||||
expect.objectContaining({
|
||||
imports: expect.any(Map),
|
||||
importedBy: expect.any(Map),
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -168,7 +172,7 @@ describe("IndexProject", () => {
|
||||
|
||||
expect(mockStorage.setProjectConfig).toHaveBeenCalledWith(
|
||||
"last_indexed",
|
||||
expect.any(Number)
|
||||
expect.any(Number),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -186,7 +190,7 @@ describe("IndexProject", () => {
|
||||
total: expect.any(Number),
|
||||
currentFile: expect.any(String),
|
||||
phase: expect.stringMatching(/scanning|parsing|analyzing|indexing/),
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -198,7 +202,7 @@ describe("IndexProject", () => {
|
||||
})
|
||||
|
||||
const scanningCalls = progressCallback.mock.calls.filter(
|
||||
(call) => call[0].phase === "scanning"
|
||||
(call) => call[0].phase === "scanning",
|
||||
)
|
||||
expect(scanningCalls.length).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -211,7 +215,7 @@ describe("IndexProject", () => {
|
||||
})
|
||||
|
||||
const parsingCalls = progressCallback.mock.calls.filter(
|
||||
(call) => call[0].phase === "parsing"
|
||||
(call) => call[0].phase === "parsing",
|
||||
)
|
||||
expect(parsingCalls.length).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -224,7 +228,7 @@ describe("IndexProject", () => {
|
||||
})
|
||||
|
||||
const analyzingCalls = progressCallback.mock.calls.filter(
|
||||
(call) => call[0].phase === "analyzing"
|
||||
(call) => call[0].phase === "analyzing",
|
||||
)
|
||||
expect(analyzingCalls.length).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -237,7 +241,7 @@ describe("IndexProject", () => {
|
||||
})
|
||||
|
||||
const indexingCalls = progressCallback.mock.calls.filter(
|
||||
(call) => call[0].phase === "indexing"
|
||||
(call) => call[0].phase === "indexing",
|
||||
)
|
||||
expect(indexingCalls.length).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -245,10 +249,7 @@ describe("IndexProject", () => {
|
||||
it("should detect TypeScript files", async () => {
|
||||
await useCase.execute("/test/project")
|
||||
|
||||
expect(mockStorage.setAST).toHaveBeenCalledWith(
|
||||
"src/index.ts",
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(mockStorage.setAST).toHaveBeenCalledWith("src/index.ts", expect.any(Object))
|
||||
})
|
||||
|
||||
it("should handle files without parseable language", async () => {
|
||||
@@ -276,7 +277,7 @@ describe("IndexProject", () => {
|
||||
|
||||
expect(mockStorage.setAST).toHaveBeenCalledWith(
|
||||
expect.stringContaining(".ts"),
|
||||
expect.any(Object)
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -294,7 +295,7 @@ describe("IndexProject", () => {
|
||||
})
|
||||
|
||||
const callsWithFiles = progressCallback.mock.calls.filter(
|
||||
(call) => call[0].currentFile && call[0].currentFile.length > 0
|
||||
(call) => call[0].currentFile && call[0].currentFile.length > 0,
|
||||
)
|
||||
expect(callsWithFiles.length).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -307,7 +308,7 @@ describe("IndexProject", () => {
|
||||
})
|
||||
|
||||
const parsingCalls = progressCallback.mock.calls.filter(
|
||||
(call) => call[0].phase === "parsing"
|
||||
(call) => call[0].phase === "parsing",
|
||||
)
|
||||
if (parsingCalls.length > 0) {
|
||||
expect(parsingCalls[0][0].total).toBe(2)
|
||||
|
||||
@@ -123,8 +123,7 @@ describe("OllamaClient", () => {
|
||||
mockOllamaInstance.chat.mockResolvedValue({
|
||||
message: {
|
||||
role: "assistant",
|
||||
content:
|
||||
'<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
|
||||
content: '<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
|
||||
tool_calls: undefined,
|
||||
},
|
||||
eval_count: 30,
|
||||
@@ -408,7 +407,6 @@ describe("OllamaClient", () => {
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should handle ECONNREFUSED errors", async () => {
|
||||
mockOllamaInstance.chat.mockRejectedValue(new Error("ECONNREFUSED"))
|
||||
@@ -435,7 +433,9 @@ describe("OllamaClient", () => {
|
||||
|
||||
const client = new OllamaClient(defaultConfig)
|
||||
|
||||
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(/Request was aborted/)
|
||||
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(
|
||||
/Request was aborted/,
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle model not found errors", async () => {
|
||||
@@ -443,7 +443,9 @@ describe("OllamaClient", () => {
|
||||
|
||||
const client = new OllamaClient(defaultConfig)
|
||||
|
||||
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(/Model.*not found/)
|
||||
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(
|
||||
/Model.*not found/,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -303,7 +303,9 @@ describe("GetFunctionTool", () => {
|
||||
})
|
||||
|
||||
it("should handle error when reading lines fails", async () => {
|
||||
const ast = createMockAST([createMockFunction({ name: "test", lineStart: 1, lineEnd: 1 })])
|
||||
const ast = createMockAST([
|
||||
createMockFunction({ name: "test", lineStart: 1, lineEnd: 1 }),
|
||||
])
|
||||
const storage: IStorage = {
|
||||
getFile: vi.fn().mockResolvedValue(null),
|
||||
getAST: vi.fn().mockResolvedValue(ast),
|
||||
|
||||
204
packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts
Normal file
204
packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Tests for AutocompleteConfigSchema.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { AutocompleteConfigSchema } from "../../../src/shared/constants/config.js"
|
||||
|
||||
describe("AutocompleteConfigSchema", () => {
|
||||
describe("default values", () => {
|
||||
it("should use defaults when empty object provided", () => {
|
||||
const result = AutocompleteConfigSchema.parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
source: "redis-index",
|
||||
maxSuggestions: 10,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use defaults via .default({})", () => {
|
||||
const result = AutocompleteConfigSchema.default({}).parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
source: "redis-index",
|
||||
maxSuggestions: 10,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("enabled", () => {
|
||||
it("should accept true", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ enabled: true })
|
||||
expect(result.enabled).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ enabled: false })
|
||||
expect(result.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ enabled: "true" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject number", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ enabled: 1 })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("source", () => {
|
||||
it("should accept redis-index", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ source: "redis-index" })
|
||||
expect(result.source).toBe("redis-index")
|
||||
})
|
||||
|
||||
it("should accept filesystem", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ source: "filesystem" })
|
||||
expect(result.source).toBe("filesystem")
|
||||
})
|
||||
|
||||
it("should accept both", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ source: "both" })
|
||||
expect(result.source).toBe("both")
|
||||
})
|
||||
|
||||
it("should use default redis-index", () => {
|
||||
const result = AutocompleteConfigSchema.parse({})
|
||||
expect(result.source).toBe("redis-index")
|
||||
})
|
||||
|
||||
it("should reject invalid source", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ source: "invalid" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-string", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ source: 123 })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("maxSuggestions", () => {
|
||||
it("should accept valid positive integer", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 5 })
|
||||
expect(result.maxSuggestions).toBe(5)
|
||||
})
|
||||
|
||||
it("should accept default value", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 10 })
|
||||
expect(result.maxSuggestions).toBe(10)
|
||||
})
|
||||
|
||||
it("should accept large value", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 100 })
|
||||
expect(result.maxSuggestions).toBe(100)
|
||||
})
|
||||
|
||||
it("should accept 1", () => {
|
||||
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 1 })
|
||||
expect(result.maxSuggestions).toBe(1)
|
||||
})
|
||||
|
||||
it("should reject zero", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: 0 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject negative number", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: -5 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject float", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: 10.5 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-number", () => {
|
||||
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: "10" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("partial config", () => {
|
||||
it("should merge partial config with defaults (enabled only)", () => {
|
||||
const result = AutocompleteConfigSchema.parse({
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: false,
|
||||
source: "redis-index",
|
||||
maxSuggestions: 10,
|
||||
})
|
||||
})
|
||||
|
||||
it("should merge partial config with defaults (source only)", () => {
|
||||
const result = AutocompleteConfigSchema.parse({
|
||||
source: "filesystem",
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
source: "filesystem",
|
||||
maxSuggestions: 10,
|
||||
})
|
||||
})
|
||||
|
||||
it("should merge partial config with defaults (maxSuggestions only)", () => {
|
||||
const result = AutocompleteConfigSchema.parse({
|
||||
maxSuggestions: 20,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
source: "redis-index",
|
||||
maxSuggestions: 20,
|
||||
})
|
||||
})
|
||||
|
||||
it("should merge multiple partial fields", () => {
|
||||
const result = AutocompleteConfigSchema.parse({
|
||||
enabled: false,
|
||||
maxSuggestions: 5,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: false,
|
||||
source: "redis-index",
|
||||
maxSuggestions: 5,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("full config", () => {
|
||||
it("should accept valid full config", () => {
|
||||
const config = {
|
||||
enabled: false,
|
||||
source: "both" as const,
|
||||
maxSuggestions: 15,
|
||||
}
|
||||
|
||||
const result = AutocompleteConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
|
||||
it("should accept all defaults explicitly", () => {
|
||||
const config = {
|
||||
enabled: true,
|
||||
source: "redis-index" as const,
|
||||
maxSuggestions: 10,
|
||||
}
|
||||
|
||||
const result = AutocompleteConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
|
||||
it("should accept filesystem as source", () => {
|
||||
const config = {
|
||||
enabled: true,
|
||||
source: "filesystem" as const,
|
||||
maxSuggestions: 20,
|
||||
}
|
||||
|
||||
const result = AutocompleteConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
221
packages/ipuaro/tests/unit/shared/context-config.test.ts
Normal file
221
packages/ipuaro/tests/unit/shared/context-config.test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Tests for ContextConfigSchema.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { ContextConfigSchema } from "../../../src/shared/constants/config.js"
|
||||
|
||||
describe("ContextConfigSchema", () => {
|
||||
describe("default values", () => {
|
||||
it("should use defaults when empty object provided", () => {
|
||||
const result = ContextConfigSchema.parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
systemPromptTokens: 2000,
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary",
|
||||
})
|
||||
})
|
||||
|
||||
it("should use defaults via .default({})", () => {
|
||||
const result = ContextConfigSchema.default({}).parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
systemPromptTokens: 2000,
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("systemPromptTokens", () => {
|
||||
it("should accept valid positive integer", () => {
|
||||
const result = ContextConfigSchema.parse({ systemPromptTokens: 1500 })
|
||||
expect(result.systemPromptTokens).toBe(1500)
|
||||
})
|
||||
|
||||
it("should accept default value", () => {
|
||||
const result = ContextConfigSchema.parse({ systemPromptTokens: 2000 })
|
||||
expect(result.systemPromptTokens).toBe(2000)
|
||||
})
|
||||
|
||||
it("should accept large value", () => {
|
||||
const result = ContextConfigSchema.parse({ systemPromptTokens: 5000 })
|
||||
expect(result.systemPromptTokens).toBe(5000)
|
||||
})
|
||||
|
||||
it("should reject zero", () => {
|
||||
expect(() => ContextConfigSchema.parse({ systemPromptTokens: 0 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject negative number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ systemPromptTokens: -100 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject float", () => {
|
||||
expect(() => ContextConfigSchema.parse({ systemPromptTokens: 1500.5 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ systemPromptTokens: "2000" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("maxContextUsage", () => {
|
||||
it("should accept valid ratio", () => {
|
||||
const result = ContextConfigSchema.parse({ maxContextUsage: 0.7 })
|
||||
expect(result.maxContextUsage).toBe(0.7)
|
||||
})
|
||||
|
||||
it("should accept default value", () => {
|
||||
const result = ContextConfigSchema.parse({ maxContextUsage: 0.8 })
|
||||
expect(result.maxContextUsage).toBe(0.8)
|
||||
})
|
||||
|
||||
it("should accept minimum value (0)", () => {
|
||||
const result = ContextConfigSchema.parse({ maxContextUsage: 0 })
|
||||
expect(result.maxContextUsage).toBe(0)
|
||||
})
|
||||
|
||||
it("should accept maximum value (1)", () => {
|
||||
const result = ContextConfigSchema.parse({ maxContextUsage: 1 })
|
||||
expect(result.maxContextUsage).toBe(1)
|
||||
})
|
||||
|
||||
it("should reject value above 1", () => {
|
||||
expect(() => ContextConfigSchema.parse({ maxContextUsage: 1.1 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject negative value", () => {
|
||||
expect(() => ContextConfigSchema.parse({ maxContextUsage: -0.1 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ maxContextUsage: "0.8" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("autoCompressAt", () => {
|
||||
it("should accept valid ratio", () => {
|
||||
const result = ContextConfigSchema.parse({ autoCompressAt: 0.75 })
|
||||
expect(result.autoCompressAt).toBe(0.75)
|
||||
})
|
||||
|
||||
it("should accept default value", () => {
|
||||
const result = ContextConfigSchema.parse({ autoCompressAt: 0.8 })
|
||||
expect(result.autoCompressAt).toBe(0.8)
|
||||
})
|
||||
|
||||
it("should accept minimum value (0)", () => {
|
||||
const result = ContextConfigSchema.parse({ autoCompressAt: 0 })
|
||||
expect(result.autoCompressAt).toBe(0)
|
||||
})
|
||||
|
||||
it("should accept maximum value (1)", () => {
|
||||
const result = ContextConfigSchema.parse({ autoCompressAt: 1 })
|
||||
expect(result.autoCompressAt).toBe(1)
|
||||
})
|
||||
|
||||
it("should reject value above 1", () => {
|
||||
expect(() => ContextConfigSchema.parse({ autoCompressAt: 1.5 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject negative value", () => {
|
||||
expect(() => ContextConfigSchema.parse({ autoCompressAt: -0.5 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ autoCompressAt: "0.8" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("compressionMethod", () => {
|
||||
it("should accept llm-summary", () => {
|
||||
const result = ContextConfigSchema.parse({ compressionMethod: "llm-summary" })
|
||||
expect(result.compressionMethod).toBe("llm-summary")
|
||||
})
|
||||
|
||||
it("should accept truncate", () => {
|
||||
const result = ContextConfigSchema.parse({ compressionMethod: "truncate" })
|
||||
expect(result.compressionMethod).toBe("truncate")
|
||||
})
|
||||
|
||||
it("should reject invalid method", () => {
|
||||
expect(() => ContextConfigSchema.parse({ compressionMethod: "invalid" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-string", () => {
|
||||
expect(() => ContextConfigSchema.parse({ compressionMethod: 123 })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("partial config", () => {
|
||||
it("should merge partial config with defaults (systemPromptTokens)", () => {
|
||||
const result = ContextConfigSchema.parse({
|
||||
systemPromptTokens: 3000,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
systemPromptTokens: 3000,
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary",
|
||||
})
|
||||
})
|
||||
|
||||
it("should merge partial config with defaults (autoCompressAt)", () => {
|
||||
const result = ContextConfigSchema.parse({
|
||||
autoCompressAt: 0.9,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
systemPromptTokens: 2000,
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.9,
|
||||
compressionMethod: "llm-summary",
|
||||
})
|
||||
})
|
||||
|
||||
it("should merge multiple partial fields", () => {
|
||||
const result = ContextConfigSchema.parse({
|
||||
maxContextUsage: 0.7,
|
||||
compressionMethod: "truncate",
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
systemPromptTokens: 2000,
|
||||
maxContextUsage: 0.7,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "truncate",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("full config", () => {
|
||||
it("should accept valid full config", () => {
|
||||
const config = {
|
||||
systemPromptTokens: 3000,
|
||||
maxContextUsage: 0.9,
|
||||
autoCompressAt: 0.85,
|
||||
compressionMethod: "truncate" as const,
|
||||
}
|
||||
|
||||
const result = ContextConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
|
||||
it("should accept all defaults explicitly", () => {
|
||||
const config = {
|
||||
systemPromptTokens: 2000,
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary" as const,
|
||||
}
|
||||
|
||||
const result = ContextConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
150
packages/ipuaro/tests/unit/shared/display-config.test.ts
Normal file
150
packages/ipuaro/tests/unit/shared/display-config.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Tests for DisplayConfigSchema.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { DisplayConfigSchema } from "../../../src/shared/constants/config.js"
|
||||
|
||||
describe("DisplayConfigSchema", () => {
|
||||
describe("default values", () => {
|
||||
it("should use defaults when empty object provided", () => {
|
||||
const result = DisplayConfigSchema.parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
showStats: true,
|
||||
showToolCalls: true,
|
||||
theme: "dark",
|
||||
bellOnComplete: false,
|
||||
progressBar: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use defaults via .default({})", () => {
|
||||
const result = DisplayConfigSchema.default({}).parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
showStats: true,
|
||||
showToolCalls: true,
|
||||
theme: "dark",
|
||||
bellOnComplete: false,
|
||||
progressBar: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("showStats", () => {
|
||||
it("should accept true", () => {
|
||||
const result = DisplayConfigSchema.parse({ showStats: true })
|
||||
expect(result.showStats).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = DisplayConfigSchema.parse({ showStats: false })
|
||||
expect(result.showStats).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => DisplayConfigSchema.parse({ showStats: "yes" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("showToolCalls", () => {
|
||||
it("should accept true", () => {
|
||||
const result = DisplayConfigSchema.parse({ showToolCalls: true })
|
||||
expect(result.showToolCalls).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = DisplayConfigSchema.parse({ showToolCalls: false })
|
||||
expect(result.showToolCalls).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => DisplayConfigSchema.parse({ showToolCalls: "yes" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("theme", () => {
|
||||
it("should accept dark", () => {
|
||||
const result = DisplayConfigSchema.parse({ theme: "dark" })
|
||||
expect(result.theme).toBe("dark")
|
||||
})
|
||||
|
||||
it("should accept light", () => {
|
||||
const result = DisplayConfigSchema.parse({ theme: "light" })
|
||||
expect(result.theme).toBe("light")
|
||||
})
|
||||
|
||||
it("should reject invalid theme", () => {
|
||||
expect(() => DisplayConfigSchema.parse({ theme: "blue" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-string", () => {
|
||||
expect(() => DisplayConfigSchema.parse({ theme: 123 })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("bellOnComplete", () => {
|
||||
it("should accept true", () => {
|
||||
const result = DisplayConfigSchema.parse({ bellOnComplete: true })
|
||||
expect(result.bellOnComplete).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = DisplayConfigSchema.parse({ bellOnComplete: false })
|
||||
expect(result.bellOnComplete).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => DisplayConfigSchema.parse({ bellOnComplete: "yes" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("progressBar", () => {
|
||||
it("should accept true", () => {
|
||||
const result = DisplayConfigSchema.parse({ progressBar: true })
|
||||
expect(result.progressBar).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = DisplayConfigSchema.parse({ progressBar: false })
|
||||
expect(result.progressBar).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => DisplayConfigSchema.parse({ progressBar: "yes" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("partial config", () => {
|
||||
it("should merge partial config with defaults", () => {
|
||||
const result = DisplayConfigSchema.parse({
|
||||
theme: "light",
|
||||
bellOnComplete: true,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
showStats: true,
|
||||
showToolCalls: true,
|
||||
theme: "light",
|
||||
bellOnComplete: true,
|
||||
progressBar: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("full config", () => {
|
||||
it("should accept valid full config", () => {
|
||||
const config = {
|
||||
showStats: false,
|
||||
showToolCalls: false,
|
||||
theme: "light" as const,
|
||||
bellOnComplete: true,
|
||||
progressBar: false,
|
||||
}
|
||||
|
||||
const result = DisplayConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
146
packages/ipuaro/tests/unit/shared/session-config.test.ts
Normal file
146
packages/ipuaro/tests/unit/shared/session-config.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Tests for SessionConfigSchema.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { SessionConfigSchema } from "../../../src/shared/constants/config.js"
|
||||
|
||||
describe("SessionConfigSchema", () => {
|
||||
describe("default values", () => {
|
||||
it("should use defaults when empty object provided", () => {
|
||||
const result = SessionConfigSchema.parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
persistIndefinitely: true,
|
||||
maxHistoryMessages: 100,
|
||||
saveInputHistory: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use defaults via .default({})", () => {
|
||||
const result = SessionConfigSchema.default({}).parse({})
|
||||
|
||||
expect(result).toEqual({
|
||||
persistIndefinitely: true,
|
||||
maxHistoryMessages: 100,
|
||||
saveInputHistory: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("persistIndefinitely", () => {
|
||||
it("should accept true", () => {
|
||||
const result = SessionConfigSchema.parse({ persistIndefinitely: true })
|
||||
expect(result.persistIndefinitely).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = SessionConfigSchema.parse({ persistIndefinitely: false })
|
||||
expect(result.persistIndefinitely).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => SessionConfigSchema.parse({ persistIndefinitely: "yes" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("maxHistoryMessages", () => {
|
||||
it("should accept valid positive integer", () => {
|
||||
const result = SessionConfigSchema.parse({ maxHistoryMessages: 50 })
|
||||
expect(result.maxHistoryMessages).toBe(50)
|
||||
})
|
||||
|
||||
it("should accept default value", () => {
|
||||
const result = SessionConfigSchema.parse({ maxHistoryMessages: 100 })
|
||||
expect(result.maxHistoryMessages).toBe(100)
|
||||
})
|
||||
|
||||
it("should accept large value", () => {
|
||||
const result = SessionConfigSchema.parse({ maxHistoryMessages: 1000 })
|
||||
expect(result.maxHistoryMessages).toBe(1000)
|
||||
})
|
||||
|
||||
it("should reject zero", () => {
|
||||
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: 0 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject negative number", () => {
|
||||
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: -10 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject float", () => {
|
||||
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: 10.5 })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject non-number", () => {
|
||||
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: "100" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("saveInputHistory", () => {
|
||||
it("should accept true", () => {
|
||||
const result = SessionConfigSchema.parse({ saveInputHistory: true })
|
||||
expect(result.saveInputHistory).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = SessionConfigSchema.parse({ saveInputHistory: false })
|
||||
expect(result.saveInputHistory).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => SessionConfigSchema.parse({ saveInputHistory: "yes" })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("partial config", () => {
|
||||
it("should merge partial config with defaults", () => {
|
||||
const result = SessionConfigSchema.parse({
|
||||
maxHistoryMessages: 50,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
persistIndefinitely: true,
|
||||
maxHistoryMessages: 50,
|
||||
saveInputHistory: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should merge multiple partial fields", () => {
|
||||
const result = SessionConfigSchema.parse({
|
||||
persistIndefinitely: false,
|
||||
saveInputHistory: false,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
persistIndefinitely: false,
|
||||
maxHistoryMessages: 100,
|
||||
saveInputHistory: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("full config", () => {
|
||||
it("should accept valid full config", () => {
|
||||
const config = {
|
||||
persistIndefinitely: false,
|
||||
maxHistoryMessages: 200,
|
||||
saveInputHistory: false,
|
||||
}
|
||||
|
||||
const result = SessionConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
|
||||
it("should accept all defaults explicitly", () => {
|
||||
const config = {
|
||||
persistIndefinitely: true,
|
||||
maxHistoryMessages: 100,
|
||||
saveInputHistory: true,
|
||||
}
|
||||
|
||||
const result = SessionConfigSchema.parse(config)
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -218,28 +218,32 @@ describe("Input", () => {
|
||||
it("should be active when multiline is true", () => {
|
||||
const multiline = true
|
||||
const lines = ["single line"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
const isMultilineActive =
|
||||
multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(true)
|
||||
})
|
||||
|
||||
it("should not be active when multiline is false", () => {
|
||||
const multiline = false
|
||||
const lines = ["line1", "line2"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
const isMultilineActive =
|
||||
multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(false)
|
||||
})
|
||||
|
||||
it("should be active in auto mode with multiple lines", () => {
|
||||
const multiline = "auto"
|
||||
const lines = ["line1", "line2"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
const isMultilineActive =
|
||||
multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(true)
|
||||
})
|
||||
|
||||
it("should not be active in auto mode with single line", () => {
|
||||
const multiline = "auto"
|
||||
const lines = ["single line"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
const isMultilineActive =
|
||||
multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
29
packages/ipuaro/tests/unit/tui/utils/bell.test.ts
Normal file
29
packages/ipuaro/tests/unit/tui/utils/bell.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Tests for bell utility.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { ringBell } from "../../../../src/tui/utils/bell.js"
|
||||
|
||||
describe("ringBell", () => {
|
||||
it("should write bell character to stdout", () => {
|
||||
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
|
||||
|
||||
ringBell()
|
||||
|
||||
expect(writeSpy).toHaveBeenCalledWith("\u0007")
|
||||
writeSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should write correct ASCII bell character", () => {
|
||||
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
|
||||
|
||||
ringBell()
|
||||
|
||||
const callArg = writeSpy.mock.calls[0]?.[0]
|
||||
expect(callArg).toBe("\u0007")
|
||||
expect(callArg?.charCodeAt(0)).toBe(7)
|
||||
|
||||
writeSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
163
packages/ipuaro/tests/unit/tui/utils/theme.test.ts
Normal file
163
packages/ipuaro/tests/unit/tui/utils/theme.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Tests for theme utilities.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
getColorScheme,
|
||||
getContextColor,
|
||||
getRoleColor,
|
||||
getStatusColor,
|
||||
} from "../../../../src/tui/utils/theme.js"
|
||||
|
||||
describe("theme utilities", () => {
|
||||
describe("getColorScheme", () => {
|
||||
it("should return dark theme colors for dark", () => {
|
||||
const scheme = getColorScheme("dark")
|
||||
|
||||
expect(scheme).toEqual({
|
||||
primary: "cyan",
|
||||
secondary: "blue",
|
||||
success: "green",
|
||||
warning: "yellow",
|
||||
error: "red",
|
||||
info: "cyan",
|
||||
muted: "gray",
|
||||
background: "black",
|
||||
foreground: "white",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return light theme colors for light", () => {
|
||||
const scheme = getColorScheme("light")
|
||||
|
||||
expect(scheme).toEqual({
|
||||
primary: "blue",
|
||||
secondary: "cyan",
|
||||
success: "green",
|
||||
warning: "yellow",
|
||||
error: "red",
|
||||
info: "blue",
|
||||
muted: "gray",
|
||||
background: "white",
|
||||
foreground: "black",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getStatusColor", () => {
|
||||
it("should return success color for ready status", () => {
|
||||
const color = getStatusColor("ready", "dark")
|
||||
expect(color).toBe("green")
|
||||
})
|
||||
|
||||
it("should return warning color for thinking status", () => {
|
||||
const color = getStatusColor("thinking", "dark")
|
||||
expect(color).toBe("yellow")
|
||||
})
|
||||
|
||||
it("should return warning color for tool_call status", () => {
|
||||
const color = getStatusColor("tool_call", "dark")
|
||||
expect(color).toBe("yellow")
|
||||
})
|
||||
|
||||
it("should return info color for awaiting_confirmation status", () => {
|
||||
const color = getStatusColor("awaiting_confirmation", "dark")
|
||||
expect(color).toBe("cyan")
|
||||
})
|
||||
|
||||
it("should return error color for error status", () => {
|
||||
const color = getStatusColor("error", "dark")
|
||||
expect(color).toBe("red")
|
||||
})
|
||||
|
||||
it("should use light theme colors when theme is light", () => {
|
||||
const color = getStatusColor("awaiting_confirmation", "light")
|
||||
expect(color).toBe("blue")
|
||||
})
|
||||
|
||||
it("should use dark theme by default", () => {
|
||||
const color = getStatusColor("ready")
|
||||
expect(color).toBe("green")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRoleColor", () => {
|
||||
it("should return success color for user role", () => {
|
||||
const color = getRoleColor("user", "dark")
|
||||
expect(color).toBe("green")
|
||||
})
|
||||
|
||||
it("should return primary color for assistant role", () => {
|
||||
const color = getRoleColor("assistant", "dark")
|
||||
expect(color).toBe("cyan")
|
||||
})
|
||||
|
||||
it("should return muted color for system role", () => {
|
||||
const color = getRoleColor("system", "dark")
|
||||
expect(color).toBe("gray")
|
||||
})
|
||||
|
||||
it("should return secondary color for tool role", () => {
|
||||
const color = getRoleColor("tool", "dark")
|
||||
expect(color).toBe("blue")
|
||||
})
|
||||
|
||||
it("should use light theme colors when theme is light", () => {
|
||||
const color = getRoleColor("assistant", "light")
|
||||
expect(color).toBe("blue")
|
||||
})
|
||||
|
||||
it("should use dark theme by default", () => {
|
||||
const color = getRoleColor("user")
|
||||
expect(color).toBe("green")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getContextColor", () => {
|
||||
it("should return success color for low usage", () => {
|
||||
const color = getContextColor(0.5, "dark")
|
||||
expect(color).toBe("green")
|
||||
})
|
||||
|
||||
it("should return warning color for medium usage", () => {
|
||||
const color = getContextColor(0.7, "dark")
|
||||
expect(color).toBe("yellow")
|
||||
})
|
||||
|
||||
it("should return error color for high usage", () => {
|
||||
const color = getContextColor(0.9, "dark")
|
||||
expect(color).toBe("red")
|
||||
})
|
||||
|
||||
it("should return success color at 59% usage", () => {
|
||||
const color = getContextColor(0.59, "dark")
|
||||
expect(color).toBe("green")
|
||||
})
|
||||
|
||||
it("should return warning color at 60% usage", () => {
|
||||
const color = getContextColor(0.6, "dark")
|
||||
expect(color).toBe("yellow")
|
||||
})
|
||||
|
||||
it("should return warning color at 79% usage", () => {
|
||||
const color = getContextColor(0.79, "dark")
|
||||
expect(color).toBe("yellow")
|
||||
})
|
||||
|
||||
it("should return error color at 80% usage", () => {
|
||||
const color = getContextColor(0.8, "dark")
|
||||
expect(color).toBe("red")
|
||||
})
|
||||
|
||||
it("should use light theme colors when theme is light", () => {
|
||||
const color = getContextColor(0.7, "light")
|
||||
expect(color).toBe("yellow")
|
||||
})
|
||||
|
||||
it("should use dark theme by default", () => {
|
||||
const color = getContextColor(0.5)
|
||||
expect(color).toBe("green")
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user