mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(ipuaro): add autocomplete configuration
- Add AutocompleteConfigSchema with enabled, source, maxSuggestions - Update useAutocomplete hook to read from config - Add 27 unit tests for autocomplete config - Fix unused variable in Chat component - Update ROADMAP and CHANGELOG
This commit is contained in:
@@ -5,6 +5,51 @@ 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/),
|
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).
|
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
|
## [0.22.3] - 2025-12-02 - Context Configuration
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1648,7 +1648,7 @@ interface DiffViewProps {
|
|||||||
## Version 0.22.0 - Extended Configuration ⚙️
|
## Version 0.22.0 - Extended Configuration ⚙️
|
||||||
|
|
||||||
**Priority:** MEDIUM
|
**Priority:** MEDIUM
|
||||||
**Status:** In Progress (3/5 complete)
|
**Status:** In Progress (4/5 complete)
|
||||||
|
|
||||||
### 0.22.1 - Display Configuration ✅
|
### 0.22.1 - Display Configuration ✅
|
||||||
|
|
||||||
@@ -1705,7 +1705,7 @@ export const ContextConfigSchema = z.object({
|
|||||||
- [x] Configurable compression threshold
|
- [x] Configurable compression threshold
|
||||||
- [x] Unit tests (40 new tests: 32 schema, 8 ContextManager integration)
|
- [x] Unit tests (40 new tests: 32 schema, 8 ContextManager integration)
|
||||||
|
|
||||||
### 0.22.4 - Autocomplete Configuration
|
### 0.22.4 - Autocomplete Configuration ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/shared/constants/config.ts additions
|
// src/shared/constants/config.ts additions
|
||||||
@@ -1717,9 +1717,9 @@ export const AutocompleteConfigSchema = z.object({
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] AutocompleteConfigSchema in config.ts
|
- [x] AutocompleteConfigSchema in config.ts
|
||||||
- [ ] useAutocomplete reads from config
|
- [x] useAutocomplete reads from config
|
||||||
- [ ] Unit tests
|
- [x] Unit tests (27 tests)
|
||||||
|
|
||||||
### 0.22.5 - Commands Configuration
|
### 0.22.5 - Commands Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/ipuaro",
|
"name": "@samiyev/ipuaro",
|
||||||
"version": "0.22.2",
|
"version": "0.22.3",
|
||||||
"description": "Local AI agent for codebase operations with infinite context feeling",
|
"description": "Local AI agent for codebase operations with infinite context feeling",
|
||||||
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -116,6 +116,15 @@ export const ContextConfigSchema = z.object({
|
|||||||
compressionMethod: z.enum(["llm-summary", "truncate"]).default("llm-summary"),
|
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.
|
* Full configuration schema.
|
||||||
*/
|
*/
|
||||||
@@ -130,6 +139,7 @@ export const ConfigSchema = z.object({
|
|||||||
display: DisplayConfigSchema.default({}),
|
display: DisplayConfigSchema.default({}),
|
||||||
session: SessionConfigSchema.default({}),
|
session: SessionConfigSchema.default({}),
|
||||||
context: ContextConfigSchema.default({}),
|
context: ContextConfigSchema.default({}),
|
||||||
|
autocomplete: AutocompleteConfigSchema.default({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,6 +156,7 @@ export type InputConfig = z.infer<typeof InputConfigSchema>
|
|||||||
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
||||||
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
||||||
export type ContextConfig = z.infer<typeof ContextConfigSchema>
|
export type ContextConfig = z.infer<typeof ContextConfigSchema>
|
||||||
|
export type AutocompleteConfig = z.infer<typeof AutocompleteConfigSchema>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default configuration.
|
* Default configuration.
|
||||||
|
|||||||
@@ -120,9 +120,7 @@ function AssistantMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolMessage({ message, theme }: MessageComponentProps): React.JSX.Element {
|
function ToolMessage({ message }: MessageComponentProps): React.JSX.Element {
|
||||||
const roleColor = getRoleColor("tool", theme)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
||||||
{message.toolResults?.map((result) => (
|
{message.toolResults?.map((result) => (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import type { IStorage } from "../../domain/services/IStorage.js"
|
import type { IStorage } from "../../domain/services/IStorage.js"
|
||||||
|
import type { AutocompleteConfig } from "../../shared/constants/config.js"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
||||||
export interface UseAutocompleteOptions {
|
export interface UseAutocompleteOptions {
|
||||||
@@ -12,6 +13,7 @@ export interface UseAutocompleteOptions {
|
|||||||
projectRoot: string
|
projectRoot: string
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
maxSuggestions?: number
|
maxSuggestions?: number
|
||||||
|
config?: AutocompleteConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseAutocompleteReturn {
|
export interface UseAutocompleteReturn {
|
||||||
@@ -107,13 +109,18 @@ function getCommonPrefix(suggestions: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAutocomplete(options: UseAutocompleteOptions): UseAutocompleteReturn {
|
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 [filePaths, setFilePaths] = useState<string[]>([])
|
||||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||||
|
|
||||||
// Load file paths from storage
|
// Load file paths from storage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) {
|
if (!isEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +142,11 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
|
|||||||
loadPaths().catch(() => {
|
loadPaths().catch(() => {
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
})
|
})
|
||||||
}, [storage, projectRoot, enabled])
|
}, [storage, projectRoot, isEnabled])
|
||||||
|
|
||||||
const complete = useCallback(
|
const complete = useCallback(
|
||||||
(partial: string): string[] => {
|
(partial: string): string[] => {
|
||||||
if (!enabled || !partial.trim()) {
|
if (!isEnabled || !partial.trim()) {
|
||||||
setSuggestions([])
|
setSuggestions([])
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -154,13 +161,13 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
|
|||||||
}))
|
}))
|
||||||
.filter((item) => item.score > 0)
|
.filter((item) => item.score > 0)
|
||||||
.sort((a, b) => b.score - a.score)
|
.sort((a, b) => b.score - a.score)
|
||||||
.slice(0, maxSuggestions)
|
.slice(0, maxSuggestionsCount)
|
||||||
.map((item) => item.path)
|
.map((item) => item.path)
|
||||||
|
|
||||||
setSuggestions(scored)
|
setSuggestions(scored)
|
||||||
return scored
|
return scored
|
||||||
},
|
},
|
||||||
[enabled, filePaths, maxSuggestions],
|
[isEnabled, filePaths, maxSuggestionsCount],
|
||||||
)
|
)
|
||||||
|
|
||||||
const accept = useCallback(
|
const accept = useCallback(
|
||||||
|
|||||||
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user