diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 2173966..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# AGENTS.md - -AI agent guidance for the Ghost codebase. - -## Project Overview - -**Ghost** is a local AI assistant CLI tool built with Go, powered by Ollama. -Inspired by cyberpunk media (Shadowrun, Cyberpunk 2077, The Matrix). - -## Code Conventions - -**Style**: - -- Standard Go formatting (enforced by pre-commit) -- Wrap errors with `fmt.Errorf("%w", err)` -- Follow Go naming conventions (exported vs unexported) -- Comment struct fields and exported types - -**Testing**: - -- Use table-driven tests pattern -- Use `errors.Is()` for error comparison -- Use `t.Fatalf()` for unexpected errors, `t.Errorf()` for assertions - -**Commit Messages**: Conventional commits (`feat:`, `fix:`, `refactor:`, -`test:`, `docs:`) - -## Design Principles - -- **Keep it simple**: Single-file structure unless strong reason to split -- **Cyberpunk aesthetic**: Match tone in user-facing messages -- **CLI-first**: Prioritize terminal experience - -## Agent Behavior - -- **Teach, don't implement**: Unless explicitly asked to edit files, teach the - user step-by-step how to implement changes themselves -- **Show code examples**: Provide complete, accurate code snippets in explanations -- **Explain the why**: Help the user understand patterns and best practices, not - just what to type diff --git a/CLAUDE.md b/CLAUDE.md index 12241b3..8c4fab1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,113 +1,36 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with - code in this repository. - ## Project Overview -Ghost is a command-line AI assistant written in Go and powered by Ollama, designed - with a cyberpunk aesthetic inspired by Shadowrun, Cyberpunk 2077, and The Matrix. - It provides local AI capabilities for querying, analyzing piped data, processing - images with vision models, and formatting output (text, JSON, Markdown). - -## Architecture - -### Core Flow +Ghost is a command line AI assistant written in Go and powered by Ollama, designed +with a cyberpunk aesthetic inspired by Shadowrun, Cyberpunk 2077, and The Matrix. -1. **Entry Point** (`main.go`): Initializes root command via Fang CLI framework - with custom theming and error handling -2. **Root Command** (`cmd/root.go`): Orchestrates the main execution flow: - - Collects user prompt, piped input, and flags - - Analyzes images if provided (using vision model) - - Executes tool calls in a loop before streaming final response - - Streams LLM response using Bubbletea TUI - - Renders final output with appropriate formatting -3. **LLM Client** (`internal/llm/ollama.go`): Communicates with Ollama API - - `StreamChat()`: Streaming chat with callback for each chunk - - `AnalyzeImages()`: Non-streaming vision model requests -4. **UI Layer** (`internal/ui/`): Bubbletea models for interactive display - - `stream.go`: Streaming model for single-shot queries - - `chat.go`: Core ChatModel struct, types, Init, Update, View - - `chat_normal.go`: Normal mode key handling - - `chat_command.go`: Command mode (`:` commands like `:q`, `:r`) - - `chat_insert.go`: Insert mode text input handling - - `chat_stream.go`: LLM streaming and response handling -5. **Theme System** (`theme/`): Handles cyberpunk-themed rendering and formatting - - UI glyphs in `theme/glyph.go`: Use `theme.GlyphInfo` (󱙝) and `theme.GlyphError` - (󱙜) +## Configuration -### Configuration System - -Configuration priority (highest to lowest): +Priority (highest to lowest): 1. Command-line flags 2. Environment variables (prefixed with `GHOST_`, dots/hyphens replaced with `*`) 3. Config file (`~/.config/ghost/config.toml`) -Implemented in `cmd/config.go` using Viper. Vision model configuration uses - nested structure: `vision.model` in config file, `--vision-model` flag, or - `GHOST_VISION*MODEL` env var. Web search uses `search.api-key` and - `search.max-results` following the same pattern. - -### Message Flow for Images - -Images are base64 encoded and analyzed separately with the vision model. Analysis - results are formatted with IMAGE_ANALYSIS blocks and appended to message history - before the main model processes everything. - -Vision system prompt is designed to prevent prompt injection from image text by - treating all visible text as data, not instructions. - -### Streaming Architecture - -User goroutine and Bubbletea message passing where callbacks send chunk/done/error - messages to the SteamModel for incremental rendering. - -### Error Handling Pattern - -All packages define custom error types (e.g., `ErrImageAnalysis`, `ErrModelNotFound`) - with cyberpunk-themed messages. Errors are wrapped using `fmt.Errorf("%w", err)` - for proper unwrapping. Theme package provides custom Fang error handler. +Nested config keys use dot notation in TOML, hyphens in flags, and `*` in env +vars (e.g., `vision.model` / `--vision-model` / `GHOST_VISION*MODEL`). ## Code Conventions -**Style**: - - Standard Go formatting (enforced by pre-commit) - Wrap errors with `fmt.Errorf("%w", err)` for proper error chains -- Follow Go naming conventions (exported vs unexported) -- Comment struct fields and exported types -- Cyberpunk aesthetic in user-facing messages (e.g., "neural link", "data stream", - "visual recon") - -**Testing**: - -- One test function per code function: Test function name matches the function -being tested +- Cyberpunk aesthetic in user-facing messages (e.g., "neural link", "data stream") +- UI glyphs: `style.GlyphInfo` (󱙝) and `style.GlyphError` (󱙜) +- Table-driven tests, one test function per code function (e.g., `TestChatModel_HandleCommandMode` tests `handleCommandMode`) -- Use table-driven tests pattern (see `cmd/root_test.go`, `internal/llm/ollama_test.go`) -- Test file naming mirrors source files (e.g., `chat_command_test.go` for `chat_command.go`) +- Test file naming mirrors source files (e.g., `chat_command_test.go`) - Use `errors.Is()` for error comparison -- Use `t.Fatalf()` for unexpected errors, `t.Errorf()` for assertions - -**Commit Messages**: -Conventional commits format (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`) +- Conventional commits (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, + `perf:`, `build:`, `ci:`, `style:`) ## Design Principles -- **Keep it simple**: Single-file structure per package unless strong reason to - split -- **Cyberpunk aesthetic**: Match tone in user-facing messages and error messages -- **CLI-first**: Prioritize terminal experience with proper TTY detection -- **Teach, don't implement**: When helping users, explain patterns and provide - code examples rather than immediately editing files - -## Documentation - -### VHS Tape Files (GIF Demos) - -- Located in `documentation/` directory -- Standard settings: Fish shell, 14pt font, 1200x600 dimensions -- Use `ghost` command (not `go run .`) in demos for cleaner output -- Key timing: 500ms between user actions, 12-20s for LLM response streaming -- Generate GIFs with `vhs .tape` from the documentation directory +- **Keep it simple**: Single file structure per package unless strong reason to split +- **Cyberpunk aesthetic**: Match tone in user facing messages and error messages +- **TUI first**: Prioritize terminal experience with proper TTY detection diff --git a/cmd/chat.go b/cmd/chat.go index 3178264..560c45b 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -13,7 +13,7 @@ import ( "github.com/theantichris/ghost/v3/internal/agent" "github.com/theantichris/ghost/v3/internal/storage" "github.com/theantichris/ghost/v3/internal/tool" - "github.com/theantichris/ghost/v3/internal/ui" + "github.com/theantichris/ghost/v3/internal/tui" ) var ErrHomeDir = errors.New("failed to retrieve user home directory") @@ -59,7 +59,7 @@ func runChat(cmd *cobra.Command, args []string) error { prompts := cmd.Context().Value(promptKey{}).(agent.Prompt) - config := ui.ModelConfig{ + config := tui.ModelConfig{ Context: cmd.Context(), Logger: logger, URL: viper.GetString("url"), @@ -70,7 +70,7 @@ func runChat(cmd *cobra.Command, args []string) error { Store: store, } - chatModel := ui.NewChatModel(config) + chatModel := tui.NewChatModel(config) logger.Info("entering chat", "ollama_url", config.URL, "chat_model", config.ChatLLM, "vision_model", config.VisionLLM) program := tea.NewProgram(chatModel) diff --git a/cmd/root.go b/cmd/root.go index 5225d39..a87bb3e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,21 +15,11 @@ import ( "github.com/theantichris/ghost/v3/internal/agent" "github.com/theantichris/ghost/v3/internal/llm" "github.com/theantichris/ghost/v3/internal/tool" - "github.com/theantichris/ghost/v3/internal/ui" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/internal/tui" + "github.com/theantichris/ghost/v3/style" ) -const ( - Version = "dev" - - useText = "ghost " - shortText = "ghost is a local cyberpunk AI assistant." - longText = `Ghost is a local cyberpunk AI assistant. -Send prompts directly or pipe data through for analysis.` - exampleText = ` ghost "explain this code" < main.go - cat error.log | ghost "what's wrong here" - ghost "tell me a joke"` -) +const Version = "dev" type promptKey struct{} @@ -50,11 +40,13 @@ func NewRootCmd() (*cobra.Command, func() error, error) { var cfgFile string cmd := &cobra.Command{ - Use: useText, - Short: shortText, - Long: longText, - Example: exampleText, - Args: cobra.MinimumNArgs(1), + Use: "ghost ", + Short: "ghost is a local cyberpunk AI assistant.", + Long: "Ghost is a local cyberpunk AI Assistant.\nSend prompts directly or pipe data through for analysis.", + Example: ` ghost "explain this code" < main.go + cat error.log | ghost "what's wrong here" + ghost "tell me a joke"`, + Args: cobra.MinimumNArgs(1), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { cmd.SetContext(context.WithValue(cmd.Context(), loggerKey{}, logger)) @@ -126,7 +118,7 @@ func run(cmd *cobra.Command, args []string) error { maxResults := viper.GetInt("search.max-results") registry := tool.NewRegistry(tavilyAPIKey, maxResults, logger) - config := ui.ModelConfig{ + config := tui.ModelConfig{ Context: cmd.Context(), Prompts: cmd.Context().Value(promptKey{}).(agent.Prompt), Logger: logger, @@ -138,7 +130,7 @@ func run(cmd *cobra.Command, args []string) error { Images: images, Registry: registry, } - streamModel := ui.NewStreamModel(config) + streamModel := tui.NewStreamModel(config) var programOpts []tea.ProgramOption if ttyIn, ttyOut, err := tea.OpenTTY(); err == nil { @@ -156,12 +148,12 @@ func run(cmd *cobra.Command, args []string) error { return fmt.Errorf("%w: %w", ErrStreamDisplay, err) } - finalModel := returnedModel.(ui.StreamModel) + finalModel := returnedModel.(tui.StreamModel) if finalModel.Err != nil { return finalModel.Err } - render, err := theme.RenderContent(finalModel.Content(), format, isTTY) + render, err := style.RenderContent(finalModel.Content(), format, isTTY) if err != nil { return fmt.Errorf("%w: %w", ErrRender, err) } diff --git a/documentation/chat.gif b/documentation/chat.gif deleted file mode 100644 index a6cbf6c..0000000 Binary files a/documentation/chat.gif and /dev/null differ diff --git a/documentation/chat.tape b/documentation/chat.tape deleted file mode 100644 index d905ba0..0000000 --- a/documentation/chat.tape +++ /dev/null @@ -1,31 +0,0 @@ -Output chat.gif - -Require echo - -Set Shell "fish" -Set FontSize 14 -Set Width 1200 -Set Height 600 - -Type "ghost chat" -Sleep 500ms -Enter -Sleep 2s - -Type "i" -Sleep 500ms - -Type "What is a ghost in the machine?" -Sleep 500ms -Enter - -Sleep 20s - -Escape -Sleep 500ms - -Type ":q" -Sleep 500ms -Enter - -Sleep 500ms diff --git a/documentation/demo.gif b/documentation/demo.gif deleted file mode 100644 index 4412d03..0000000 Binary files a/documentation/demo.gif and /dev/null differ diff --git a/documentation/demo.tape b/documentation/demo.tape deleted file mode 100644 index 1d8e2ef..0000000 --- a/documentation/demo.tape +++ /dev/null @@ -1,13 +0,0 @@ -Output demo.gif - -Require echo - -Set Shell "fish" -Set FontSize 14 -Set Width 1200 -Set Height 600 - -Type "./test_features.sh" -Sleep 500ms -Enter -Sleep 5s diff --git a/documentation/file.gif b/documentation/file.gif deleted file mode 100644 index 97ed34f..0000000 Binary files a/documentation/file.gif and /dev/null differ diff --git a/documentation/file.tape b/documentation/file.tape deleted file mode 100644 index e9d91f2..0000000 --- a/documentation/file.tape +++ /dev/null @@ -1,37 +0,0 @@ -Output file.gif - -Require echo - -Set Shell "fish" -Set FontSize 14 -Set Width 1200 -Set Height 600 - -Type "ghost chat" -Sleep 500ms -Enter -Sleep 2s - -Type ":" -Sleep 500ms -Type "r /home/christopher/Code/ghost/main.go" -Sleep 500ms -Enter -Sleep 1s - -Type "i" -Sleep 500ms -Type "explain this file" -Sleep 500ms -Enter - -Sleep 20s - -Escape -Sleep 500ms - -Type ":q" -Sleep 500ms -Enter - -Sleep 500ms diff --git a/documentation/search.gif b/documentation/search.gif deleted file mode 100644 index 416a5a5..0000000 Binary files a/documentation/search.gif and /dev/null differ diff --git a/documentation/search.tape b/documentation/search.tape deleted file mode 100644 index 302ba71..0000000 --- a/documentation/search.tape +++ /dev/null @@ -1,13 +0,0 @@ -Output demo.gif - -Require echo - -Set Shell "fish" -Set FontSize 14 -Set Width 1200 -Set Height 600 - -Type "go run . 'what is the latest go programming language news?'" -Sleep 500ms -Enter -Sleep 20s diff --git a/internal/tool/registry.go b/internal/tool/registry.go index 981ca4a..4bc0cd4 100644 --- a/internal/tool/registry.go +++ b/internal/tool/registry.go @@ -12,6 +12,12 @@ import ( var ErrToolNotRegistered = errors.New("tool not registered") +// Tool is the interface that all the tools the LLM uses must implement. +type Tool interface { + Definition() llm.Tool + Execute(ctx context.Context, args json.RawMessage) (string, error) +} + // Registry holds all available tools, provides their definitions to send to chat // requests, and dispatches execution to the right tool by name. type Registry struct { diff --git a/internal/tool/tool.go b/internal/tool/tool.go deleted file mode 100644 index 8f93b00..0000000 --- a/internal/tool/tool.go +++ /dev/null @@ -1,14 +0,0 @@ -package tool - -import ( - "context" - "encoding/json" - - "github.com/theantichris/ghost/v3/internal/llm" -) - -// Tool is the interface that all the tools the LLM uses must implement. -type Tool interface { - Definition() llm.Tool - Execute(ctx context.Context, args json.RawMessage) (string, error) -} diff --git a/internal/ui/channel.go b/internal/tui/channel.go similarity index 96% rename from internal/ui/channel.go rename to internal/tui/channel.go index 8aeb7aa..b7122be 100644 --- a/internal/ui/channel.go +++ b/internal/tui/channel.go @@ -1,4 +1,4 @@ -package ui +package tui import tea "charm.land/bubbletea/v2" diff --git a/internal/ui/channel_test.go b/internal/tui/channel_test.go similarity index 99% rename from internal/ui/channel_test.go rename to internal/tui/channel_test.go index 73c3b0a..fe7cda2 100644 --- a/internal/ui/channel_test.go +++ b/internal/tui/channel_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "testing" diff --git a/internal/ui/chat.go b/internal/tui/chat.go similarity index 97% rename from internal/ui/chat.go rename to internal/tui/chat.go index c5c67fc..56b7b53 100644 --- a/internal/ui/chat.go +++ b/internal/tui/chat.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "context" @@ -14,7 +14,7 @@ import ( "github.com/theantichris/ghost/v3/internal/llm" "github.com/theantichris/ghost/v3/internal/storage" "github.com/theantichris/ghost/v3/internal/tool" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/style" ) // Mode represents the different modes the TUI can be in. @@ -162,7 +162,7 @@ func (model ChatModel) View() tea.View { var view tea.View if !model.ready { - view = tea.NewView(theme.GlyphInfo + " initializing...") + view = tea.NewView(style.GlyphInfo + " initializing...") view.AltScreen = true return view @@ -222,7 +222,7 @@ func (model ChatModel) handleThreadListMode(msg tea.KeyPressMsg) (tea.Model, tea model, err = model.loadThread(selectedThread.thread.ID) if err != nil { model.logger.Error("error loading thread", "thread_id", selectedThread.thread.ID, "error", err.Error()) - model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", theme.GlyphError, err.Error()) + model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", style.GlyphError, err.Error()) } } diff --git a/internal/ui/chat_command.go b/internal/tui/chat_command.go similarity index 88% rename from internal/ui/chat_command.go rename to internal/tui/chat_command.go index f55692a..3e64396 100644 --- a/internal/ui/chat_command.go +++ b/internal/tui/chat_command.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "fmt" @@ -7,7 +7,7 @@ import ( tea "charm.land/bubbletea/v2" "github.com/theantichris/ghost/v3/internal/agent" "github.com/theantichris/ghost/v3/internal/llm" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/style" ) func (model ChatModel) handleCommandMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { @@ -58,7 +58,7 @@ func (model ChatModel) createThreadList() (tea.Model, tea.Cmd) { threadList, err := NewThreadListModel(model.store, model.width, model.height, model.logger) if err != nil { model.logger.Error("error creating thread list", "error", err) - model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", theme.GlyphError, err.Error()) + model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", style.GlyphError, err.Error()) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal model.cmdInput.Reset() @@ -75,7 +75,7 @@ func (model ChatModel) createThreadList() (tea.Model, tea.Cmd) { func (model ChatModel) readFile(arg string) (tea.Model, tea.Cmd) { if arg == "" { - model.chatHistory += fmt.Sprintf("\n[%s error: no file path provided]\n", theme.GlyphError) + model.chatHistory += fmt.Sprintf("\n[%s error: no file path provided]\n", style.GlyphError) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal model.cmdInput.Reset() @@ -86,7 +86,7 @@ func (model ChatModel) readFile(arg string) (tea.Model, tea.Cmd) { fileType, err := agent.DetectFileType(arg) if err != nil { model.logger.Error("failed to validate file", "error", err.Error(), "path", arg) - model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", theme.GlyphError, err.Error()) + model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", style.GlyphError, err.Error()) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal model.cmdInput.Reset() @@ -96,7 +96,7 @@ func (model ChatModel) readFile(arg string) (tea.Model, tea.Cmd) { switch fileType { case agent.FileTypeDir: - model.chatHistory += fmt.Sprintf("\n[%s error: file is directory]\n", theme.GlyphError) + model.chatHistory += fmt.Sprintf("\n[%s error: file is directory]\n", style.GlyphError) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal model.cmdInput.Reset() @@ -115,7 +115,7 @@ func (model ChatModel) analyzeImage(path string) (tea.Model, tea.Cmd) { content, err := agent.AnalyseImages(model.ctx, model.url, model.visionLLM, model.prompts, []string{path}, model.logger) if err != nil { model.logger.Error("image read failed", "path", path, "error", err) - model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", theme.GlyphError, err.Error()) + model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", style.GlyphError, err.Error()) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal model.cmdInput.Reset() @@ -126,7 +126,7 @@ func (model ChatModel) analyzeImage(path string) (tea.Model, tea.Cmd) { model.messages = append(model.messages, content...) model.logger.Info("image loaded into context", "path", path) - model.chatHistory += fmt.Sprintf("\n[%s loaded image: %s]\n", theme.GlyphInfo, path) + model.chatHistory += fmt.Sprintf("\n[%s loaded image: %s]\n", style.GlyphInfo, path) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal @@ -139,7 +139,7 @@ func (model ChatModel) readTextFile(path string) (tea.Model, tea.Cmd) { content, err := agent.ReadTextFile(path) if err != nil { model.logger.Error("file read failed", "path", path, "error", err) - model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", theme.GlyphError, err.Error()) + model.chatHistory += fmt.Sprintf("\n[%s error: %s]\n", style.GlyphError, err.Error()) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal model.cmdInput.Reset() @@ -149,7 +149,7 @@ func (model ChatModel) readTextFile(path string) (tea.Model, tea.Cmd) { model.messages = append(model.messages, llm.ChatMessage{Role: llm.RoleUser, Content: content}) model.logger.Info("loaded file", "path", path) - model.chatHistory += fmt.Sprintf("\n[%s loaded: %s]\n", theme.GlyphInfo, path) + model.chatHistory += fmt.Sprintf("\n[%s loaded: %s]\n", style.GlyphInfo, path) model.viewport.SetContent(model.renderHistory()) model.mode = ModeNormal diff --git a/internal/ui/chat_command_test.go b/internal/tui/chat_command_test.go similarity index 96% rename from internal/ui/chat_command_test.go rename to internal/tui/chat_command_test.go index 5e4224b..33ce10a 100644 --- a/internal/ui/chat_command_test.go +++ b/internal/tui/chat_command_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "fmt" @@ -9,7 +9,7 @@ import ( tea "charm.land/bubbletea/v2" "github.com/theantichris/ghost/v3/internal/llm" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/style" ) func TestChatModel_HandleCommandMode(t *testing.T) { @@ -76,7 +76,7 @@ func TestChatModel_HandleCommandMode(t *testing.T) { msg: tea.KeyPressMsg{Code: tea.KeyEnter}, wantMode: ModeNormal, wantInputValue: "", - wantChatHistoryMatch: fmt.Sprintf("[%s error: no file path provided]", theme.GlyphError), + wantChatHistoryMatch: fmt.Sprintf("[%s error: no file path provided]", style.GlyphError), wantMessageCount: 1, }, { @@ -85,7 +85,7 @@ func TestChatModel_HandleCommandMode(t *testing.T) { msg: tea.KeyPressMsg{Code: tea.KeyEnter}, wantMode: ModeNormal, wantInputValue: "", - wantChatHistoryMatch: fmt.Sprintf("[%s error: no file path provided]", theme.GlyphError), + wantChatHistoryMatch: fmt.Sprintf("[%s error: no file path provided]", style.GlyphError), wantMessageCount: 1, }, { @@ -94,7 +94,7 @@ func TestChatModel_HandleCommandMode(t *testing.T) { msg: tea.KeyPressMsg{Code: tea.KeyEnter}, wantMode: ModeNormal, wantInputValue: "", - wantChatHistoryMatch: fmt.Sprintf("[%s error:", theme.GlyphError), + wantChatHistoryMatch: fmt.Sprintf("[%s error:", style.GlyphError), wantMessageCount: 1, }, { @@ -104,7 +104,7 @@ func TestChatModel_HandleCommandMode(t *testing.T) { msg: tea.KeyPressMsg{Code: tea.KeyEnter}, wantMode: ModeNormal, wantInputValue: "", - wantChatHistoryMatch: fmt.Sprintf("[%s loaded:", theme.GlyphInfo), + wantChatHistoryMatch: fmt.Sprintf("[%s loaded:", style.GlyphInfo), wantMessageCount: 2, wantLastRole: llm.RoleUser, }, @@ -117,7 +117,7 @@ func TestChatModel_HandleCommandMode(t *testing.T) { msg: tea.KeyPressMsg{Code: tea.KeyEnter}, wantMode: ModeNormal, wantInputValue: "", - wantChatHistoryMatch: fmt.Sprintf("[%s error:", theme.GlyphError), + wantChatHistoryMatch: fmt.Sprintf("[%s error:", style.GlyphError), wantMessageCount: 1, }, { diff --git a/internal/ui/chat_insert.go b/internal/tui/chat_insert.go similarity index 99% rename from internal/ui/chat_insert.go rename to internal/tui/chat_insert.go index f9618d9..2d89e20 100644 --- a/internal/ui/chat_insert.go +++ b/internal/tui/chat_insert.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "fmt" diff --git a/internal/ui/chat_insert_test.go b/internal/tui/chat_insert_test.go similarity index 99% rename from internal/ui/chat_insert_test.go rename to internal/tui/chat_insert_test.go index 59ab6e6..a646d70 100644 --- a/internal/ui/chat_insert_test.go +++ b/internal/tui/chat_insert_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "testing" diff --git a/internal/ui/chat_normal.go b/internal/tui/chat_normal.go similarity index 98% rename from internal/ui/chat_normal.go rename to internal/tui/chat_normal.go index 4caa92e..3b55b2d 100644 --- a/internal/ui/chat_normal.go +++ b/internal/tui/chat_normal.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "charm.land/bubbles/v2/textinput" diff --git a/internal/ui/chat_normal_test.go b/internal/tui/chat_normal_test.go similarity index 99% rename from internal/ui/chat_normal_test.go rename to internal/tui/chat_normal_test.go index 719741c..dffb9b2 100644 --- a/internal/ui/chat_normal_test.go +++ b/internal/tui/chat_normal_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "testing" diff --git a/internal/ui/chat_stream.go b/internal/tui/chat_stream.go similarity index 94% rename from internal/ui/chat_stream.go rename to internal/tui/chat_stream.go index 532c444..4220baa 100644 --- a/internal/ui/chat_stream.go +++ b/internal/tui/chat_stream.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "fmt" @@ -6,7 +6,7 @@ import ( tea "charm.land/bubbletea/v2" "github.com/theantichris/ghost/v3/internal/agent" "github.com/theantichris/ghost/v3/internal/llm" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/style" ) // startLLMStream starts the LLM call in a go routine. @@ -74,7 +74,7 @@ func (model ChatModel) handleLLMDoneMsg() (tea.Model, tea.Cmd) { func (model ChatModel) handleLLMErrorMsg(msg LLMErrorMsg) (tea.Model, tea.Cmd) { model.logger.Error("neural link disrupted", "error", msg.Err) - model.chatHistory += fmt.Sprintf("\n[%s error: %v]\n", theme.GlyphInfo, msg.Err) + model.chatHistory += fmt.Sprintf("\n[%s error: %v]\n", style.GlyphInfo, msg.Err) model.viewport.SetContent(model.renderHistory()) return model, nil diff --git a/internal/ui/chat_stream_test.go b/internal/tui/chat_stream_test.go similarity index 97% rename from internal/ui/chat_stream_test.go rename to internal/tui/chat_stream_test.go index 020bc4c..a4a24e7 100644 --- a/internal/ui/chat_stream_test.go +++ b/internal/tui/chat_stream_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "errors" @@ -7,7 +7,7 @@ import ( tea "charm.land/bubbletea/v2" "github.com/theantichris/ghost/v3/internal/llm" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/style" ) func TestChatModel_HandleLLMMessages(t *testing.T) { @@ -50,7 +50,7 @@ func TestChatModel_HandleLLMMessages(t *testing.T) { currentResponse: "", chatHistory: "", msg: LLMErrorMsg{Err: errors.New("test error")}, - wantChatHistory: fmt.Sprintf("\n[%s error: test error]\n", theme.GlyphInfo), + wantChatHistory: fmt.Sprintf("\n[%s error: test error]\n", style.GlyphInfo), wantCurrentResponse: "", wantMessageCount: 1, wantCmd: false, diff --git a/internal/ui/chat_test.go b/internal/tui/chat_test.go similarity index 98% rename from internal/ui/chat_test.go rename to internal/tui/chat_test.go index 5cd4524..2bc7472 100644 --- a/internal/ui/chat_test.go +++ b/internal/tui/chat_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "context" diff --git a/internal/ui/chat_thread_list_test.go b/internal/tui/chat_thread_list_test.go similarity index 99% rename from internal/ui/chat_thread_list_test.go rename to internal/tui/chat_thread_list_test.go index 661e5a1..f215503 100644 --- a/internal/ui/chat_thread_list_test.go +++ b/internal/tui/chat_thread_list_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "testing" diff --git a/internal/ui/config.go b/internal/tui/config.go similarity index 98% rename from internal/ui/config.go rename to internal/tui/config.go index c0b1af9..92d0008 100644 --- a/internal/ui/config.go +++ b/internal/tui/config.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "context" diff --git a/internal/ui/storage.go b/internal/tui/storage.go similarity index 99% rename from internal/ui/storage.go rename to internal/tui/storage.go index a47156c..5a36d80 100644 --- a/internal/ui/storage.go +++ b/internal/tui/storage.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "fmt" diff --git a/internal/ui/storage_test.go b/internal/tui/storage_test.go similarity index 99% rename from internal/ui/storage_test.go rename to internal/tui/storage_test.go index 4a74789..bc5b6ec 100644 --- a/internal/ui/storage_test.go +++ b/internal/tui/storage_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "strings" diff --git a/internal/ui/stream.go b/internal/tui/stream.go similarity index 92% rename from internal/ui/stream.go rename to internal/tui/stream.go index fbc8132..6dca0a5 100644 --- a/internal/ui/stream.go +++ b/internal/tui/stream.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "context" @@ -10,7 +10,7 @@ import ( "github.com/theantichris/ghost/v3/internal/agent" "github.com/theantichris/ghost/v3/internal/llm" "github.com/theantichris/ghost/v3/internal/tool" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/style" ) var quitKeys = key.NewBinding( @@ -49,7 +49,7 @@ type StreamModel struct { func NewStreamModel(config ModelConfig) StreamModel { s := spinner.New() s.Spinner = spinner.Ellipsis - s.Style = theme.FgAccent0 + s.Style = style.FgAccent0 return StreamModel{ ctx: config.Context, @@ -122,20 +122,20 @@ func (model StreamModel) View() tea.View { } if model.content != "" { - content, err := theme.RenderContent(model.content, model.format, true) + content, err := style.RenderContent(model.content, model.format, true) if err != nil { model.logger.Error("content render failed", "error", err, "format", model.format) return tea.NewView("") } if model.format == "" { - content = theme.WordWrap(model.width, content, theme.FgText) + content = style.WordWrap(model.width, content, style.FgText) } return tea.NewView(content) } - processingMessage := theme.FgAccent0.Render(theme.GlyphInfo+" processing") + model.spinner.View() + processingMessage := style.FgAccent0.Render(style.GlyphInfo+" processing") + model.spinner.View() return tea.NewView(processingMessage) } @@ -147,7 +147,7 @@ func (model StreamModel) Content() string { return model.content } - return theme.WordWrap(model.width, model.content, theme.FgText) + return style.WordWrap(model.width, model.content, style.FgText) } func (model StreamModel) startStream() tea.Cmd { diff --git a/internal/ui/stream_test.go b/internal/tui/stream_test.go similarity index 99% rename from internal/ui/stream_test.go rename to internal/tui/stream_test.go index 9226f97..5c9de6b 100644 --- a/internal/ui/stream_test.go +++ b/internal/tui/stream_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "context" diff --git a/internal/ui/thread_list.go b/internal/tui/thread_list.go similarity index 99% rename from internal/ui/thread_list.go rename to internal/tui/thread_list.go index a97fc9c..28ad4c6 100644 --- a/internal/ui/thread_list.go +++ b/internal/tui/thread_list.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "charm.land/bubbles/v2/list" diff --git a/internal/ui/thread_list_item.go b/internal/tui/thread_list_item.go similarity index 97% rename from internal/ui/thread_list_item.go rename to internal/tui/thread_list_item.go index a918381..da3f137 100644 --- a/internal/ui/thread_list_item.go +++ b/internal/tui/thread_list_item.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "time" diff --git a/internal/ui/thread_list_item_test.go b/internal/tui/thread_list_item_test.go similarity index 99% rename from internal/ui/thread_list_item_test.go rename to internal/tui/thread_list_item_test.go index 56ba0b3..cb07416 100644 --- a/internal/ui/thread_list_item_test.go +++ b/internal/tui/thread_list_item_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "testing" diff --git a/internal/ui/thread_list_test.go b/internal/tui/thread_list_test.go similarity index 99% rename from internal/ui/thread_list_test.go rename to internal/tui/thread_list_test.go index b764431..e28c34b 100644 --- a/internal/ui/thread_list_test.go +++ b/internal/tui/thread_list_test.go @@ -1,4 +1,4 @@ -package ui +package tui import ( "io" diff --git a/main.go b/main.go index 579cae2..aa593d9 100644 --- a/main.go +++ b/main.go @@ -6,14 +6,14 @@ import ( "github.com/charmbracelet/fang" "github.com/theantichris/ghost/v3/cmd" - "github.com/theantichris/ghost/v3/theme" + "github.com/theantichris/ghost/v3/style" ) func main() { rootCmd, loggerCleanup, err := cmd.NewRootCmd() if err != nil { - theme.FangErrorHandler(os.Stderr, fang.Styles{}, err) + style.FangErrorHandler(os.Stderr, fang.Styles{}, err) os.Exit(1) } @@ -25,8 +25,8 @@ func main() { context.Background(), rootCmd, fang.WithVersion(rootCmd.Version), - fang.WithColorSchemeFunc(theme.GetFangColorScheme), - fang.WithErrorHandler(theme.FangErrorHandler), + fang.WithColorSchemeFunc(style.GetFangColorScheme), + fang.WithErrorHandler(style.FangErrorHandler), fang.WithNotifySignal(os.Interrupt), ); err != nil { os.Exit(1) diff --git a/theme/color.go b/style/color.go similarity index 99% rename from theme/color.go rename to style/color.go index 42243f8..dc43289 100644 --- a/theme/color.go +++ b/style/color.go @@ -1,4 +1,4 @@ -package theme +package style import "charm.land/lipgloss/v2" diff --git a/theme/fang.go b/style/fang.go similarity index 98% rename from theme/fang.go rename to style/fang.go index 84981ed..2ad8bea 100644 --- a/theme/fang.go +++ b/style/fang.go @@ -1,4 +1,4 @@ -package theme +package style import ( "fmt" diff --git a/theme/glamour.go b/style/glamour.go similarity index 99% rename from theme/glamour.go rename to style/glamour.go index af312d5..a9e89a3 100644 --- a/theme/glamour.go +++ b/style/glamour.go @@ -1,4 +1,4 @@ -package theme +package style import ( "fmt" diff --git a/theme/glyph.go b/style/glyph.go similarity index 86% rename from theme/glyph.go rename to style/glyph.go index 16af77b..0591dfa 100644 --- a/theme/glyph.go +++ b/style/glyph.go @@ -1,4 +1,4 @@ -package theme +package style // Glyphs for consistent UI messaging const ( diff --git a/theme/json.go b/style/json.go similarity index 99% rename from theme/json.go rename to style/json.go index 34bfe84..fe41f79 100644 --- a/theme/json.go +++ b/style/json.go @@ -1,4 +1,4 @@ -package theme +package style import ( "strings" diff --git a/theme/render.go b/style/render.go similarity index 98% rename from theme/render.go rename to style/render.go index daa86d5..5d6b693 100644 --- a/theme/render.go +++ b/style/render.go @@ -1,4 +1,4 @@ -package theme +package style import ( "errors" diff --git a/theme/style.go b/style/style.go similarity index 98% rename from theme/style.go rename to style/style.go index 08a9de5..5e47e4c 100644 --- a/theme/style.go +++ b/style/style.go @@ -1,4 +1,4 @@ -package theme +package style import ( "charm.land/lipgloss/v2" diff --git a/test_features.sh b/test_features.sh deleted file mode 100755 index ce9e940..0000000 --- a/test_features.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# Test script for Ghost features -# Usage: ./test_features.sh - -set -e - -echo "Testing Ghost features" -echo "================================================" -echo "" - -echo "1. Testing normal output:" -echo "------------------------" -echo "$ go run . \"say hello\"" -echo "" -go run . "say hello" -echo "" - -echo "2. Testing piping content in:" -echo "------------------------" -echo "$ echo \"package main\" | go run . \"explain this code\"" -echo "" -echo "package main" | go run . "explain this code" -echo "" - -echo "3. Testing piping content out:" -echo "------------------------" -echo "$ go run . \"say hello\" | cat" -echo "" -go run . "say hello" | cat -echo "" - -echo "4. Testing JSON output:" -echo "------------------------" -echo "$ go run . -f json \"list 3 colors\"" -echo "" -go run . -f json "list 3 colors" -echo "" - -echo "5. Testing markdown output:" -echo "------------------------" -echo "$ go run . -f markdown \"write markdown showing all these elements: heading 1, heading 2, bold, italic, code block with go code, inline code, link, list, blockquote, horizontal rule\"" -echo "" -go run . -f markdown "write markdown showing all these elements: heading 1, heading 2, bold, italic, code block with go code, inline code, link, list, blockquote, horizontal rule" -echo "" - -echo "================================================" -echo "All feature tests completed!"