From 60052c0db91042a9d3ccdc848d5edf73e57ba572 Mon Sep 17 00:00:00 2001 From: imfozilbek Date: Tue, 2 Dec 2025 02:26:36 +0500 Subject: [PATCH] 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 --- packages/ipuaro/CHANGELOG.md | 45 ++++ packages/ipuaro/ROADMAP.md | 10 +- packages/ipuaro/package.json | 2 +- .../ipuaro/src/shared/constants/config.ts | 11 + packages/ipuaro/src/tui/components/Chat.tsx | 4 +- .../ipuaro/src/tui/hooks/useAutocomplete.ts | 19 +- .../unit/shared/autocomplete-config.test.ts | 204 ++++++++++++++++++ 7 files changed, 280 insertions(+), 15 deletions(-) create mode 100644 packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts diff --git a/packages/ipuaro/CHANGELOG.md b/packages/ipuaro/CHANGELOG.md index e2d61d9..3b52a4a 100644 --- a/packages/ipuaro/CHANGELOG.md +++ b/packages/ipuaro/CHANGELOG.md @@ -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/), 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 diff --git a/packages/ipuaro/ROADMAP.md b/packages/ipuaro/ROADMAP.md index 3e0acf4..1d6405b 100644 --- a/packages/ipuaro/ROADMAP.md +++ b/packages/ipuaro/ROADMAP.md @@ -1648,7 +1648,7 @@ interface DiffViewProps { ## Version 0.22.0 - Extended Configuration ⚙️ **Priority:** MEDIUM -**Status:** In Progress (3/5 complete) +**Status:** In Progress (4/5 complete) ### 0.22.1 - Display Configuration ✅ @@ -1705,7 +1705,7 @@ export const ContextConfigSchema = z.object({ - [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 diff --git a/packages/ipuaro/package.json b/packages/ipuaro/package.json index 51fb98a..570286a 100644 --- a/packages/ipuaro/package.json +++ b/packages/ipuaro/package.json @@ -1,6 +1,6 @@ { "name": "@samiyev/ipuaro", - "version": "0.22.2", + "version": "0.22.3", "description": "Local AI agent for codebase operations with infinite context feeling", "author": "Fozilbek Samiyev ", "license": "MIT", diff --git a/packages/ipuaro/src/shared/constants/config.ts b/packages/ipuaro/src/shared/constants/config.ts index aa622a7..b9c7c83 100644 --- a/packages/ipuaro/src/shared/constants/config.ts +++ b/packages/ipuaro/src/shared/constants/config.ts @@ -116,6 +116,15 @@ export const ContextConfigSchema = z.object({ 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. */ @@ -130,6 +139,7 @@ export const ConfigSchema = z.object({ display: DisplayConfigSchema.default({}), session: SessionConfigSchema.default({}), context: ContextConfigSchema.default({}), + autocomplete: AutocompleteConfigSchema.default({}), }) /** @@ -146,6 +156,7 @@ export type InputConfig = z.infer export type DisplayConfig = z.infer export type SessionConfig = z.infer export type ContextConfig = z.infer +export type AutocompleteConfig = z.infer /** * Default configuration. diff --git a/packages/ipuaro/src/tui/components/Chat.tsx b/packages/ipuaro/src/tui/components/Chat.tsx index 179f418..8233a93 100644 --- a/packages/ipuaro/src/tui/components/Chat.tsx +++ b/packages/ipuaro/src/tui/components/Chat.tsx @@ -120,9 +120,7 @@ function AssistantMessage({ ) } -function ToolMessage({ message, theme }: MessageComponentProps): React.JSX.Element { - const roleColor = getRoleColor("tool", theme) - +function ToolMessage({ message }: MessageComponentProps): React.JSX.Element { return ( {message.toolResults?.map((result) => ( diff --git a/packages/ipuaro/src/tui/hooks/useAutocomplete.ts b/packages/ipuaro/src/tui/hooks/useAutocomplete.ts index 5cf2982..382ed12 100644 --- a/packages/ipuaro/src/tui/hooks/useAutocomplete.ts +++ b/packages/ipuaro/src/tui/hooks/useAutocomplete.ts @@ -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([]) const [suggestions, setSuggestions] = useState([]) // 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( diff --git a/packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts b/packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts new file mode 100644 index 0000000..e7588b6 --- /dev/null +++ b/packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts @@ -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) + }) + }) +})