From bbfa60c787f2ec459f1689b9a650ddbec9693ed9 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 16 Apr 2025 20:06:23 +0200 Subject: reimplement agent,provider and add file history --- internal/llm/tools/write_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'internal/llm/tools/write_test.go') diff --git a/internal/llm/tools/write_test.go b/internal/llm/tools/write_test.go index 50dafc14f..2264f36fb 100644 --- a/internal/llm/tools/write_test.go +++ b/internal/llm/tools/write_test.go @@ -14,7 +14,7 @@ import ( ) func TestWriteTool_Info(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) info := tool.Info() assert.Equal(t, WriteToolName, info.Name) @@ -32,7 +32,7 @@ func TestWriteTool_Run(t *testing.T) { defer os.RemoveAll(tempDir) t.Run("creates a new file successfully", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) filePath := filepath.Join(tempDir, "new_file.txt") content := "This is a test content" @@ -61,7 +61,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("creates file with nested directories", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt") content := "Content in nested directory" @@ -90,7 +90,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("updates existing file", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) // Create a file first filePath := filepath.Join(tempDir, "existing_file.txt") @@ -127,7 +127,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("handles invalid parameters", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) call := ToolCall{ Name: WriteToolName, @@ -140,7 +140,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("handles missing file_path", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) params := WriteParams{ FilePath: "", @@ -161,7 +161,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("handles missing content", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) params := WriteParams{ FilePath: filepath.Join(tempDir, "file.txt"), @@ -182,7 +182,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("handles writing to a directory path", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) // Create a directory dirPath := filepath.Join(tempDir, "test_dir") @@ -208,7 +208,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("handles permission denied", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(false)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(false), newMockFileHistoryService()) filePath := filepath.Join(tempDir, "permission_denied.txt") params := WriteParams{ @@ -234,7 +234,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("detects file modified since last read", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) // Create a file filePath := filepath.Join(tempDir, "modified_file.txt") @@ -275,7 +275,7 @@ func TestWriteTool_Run(t *testing.T) { }) t.Run("skips writing when content is identical", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) // Create a file filePath := filepath.Join(tempDir, "identical_content.txt") -- cgit v1.2.3 From cc07f7a186995f428436bc1adc66a264a95171a4 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 16 Apr 2025 21:48:29 +0200 Subject: rename to opencode --- .opencode.json | 11 ++++ cmd/root.go | 14 ++-- go.mod | 2 +- internal/app/app.go | 18 +++--- internal/app/lsp.go | 8 +-- internal/config/config.go | 4 +- internal/db/connect.go | 6 +- internal/diff/diff.go | 4 +- internal/history/file.go | 4 +- internal/llm/agent/agent-tool.go | 10 +-- internal/llm/agent/agent.go | 18 +++--- internal/llm/agent/mcp-tools.go | 10 +-- internal/llm/agent/tools.go | 12 ++-- internal/llm/prompt/coder.go | 97 ++++++++++++---------------- internal/llm/prompt/prompt.go | 4 +- internal/llm/prompt/task.go | 4 +- internal/llm/prompt/title.go | 2 +- internal/llm/provider/anthropic.go | 8 +-- internal/llm/provider/bedrock.go | 4 +- internal/llm/provider/gemini.go | 8 +-- internal/llm/provider/openai.go | 8 +-- internal/llm/provider/provider.go | 6 +- internal/llm/tools/bash.go | 16 ++--- internal/llm/tools/diagnostics.go | 4 +- internal/llm/tools/edit.go | 10 +-- internal/llm/tools/edit_test.go | 2 +- internal/llm/tools/fetch.go | 6 +- internal/llm/tools/glob.go | 2 +- internal/llm/tools/grep.go | 2 +- internal/llm/tools/ls.go | 2 +- internal/llm/tools/mocks_test.go | 6 +- internal/llm/tools/shell/shell.go | 8 +-- internal/llm/tools/sourcegraph.go | 2 +- internal/llm/tools/view.go | 4 +- internal/llm/tools/write.go | 10 +-- internal/llm/tools/write_test.go | 2 +- internal/logging/writer.go | 2 +- internal/lsp/client.go | 6 +- internal/lsp/handlers.go | 8 +-- internal/lsp/language.go | 2 +- internal/lsp/methods.go | 2 +- internal/lsp/transport.go | 4 +- internal/lsp/util/edit.go | 2 +- internal/lsp/watcher/watcher.go | 8 +-- internal/message/content.go | 2 +- internal/message/message.go | 6 +- internal/permission/permission.go | 2 +- internal/session/session.go | 4 +- internal/tui/components/chat/chat.go | 8 +-- internal/tui/components/chat/editor.go | 10 +-- internal/tui/components/chat/messages.go | 22 +++---- internal/tui/components/chat/sidebar.go | 12 ++-- internal/tui/components/core/status.go | 12 ++-- internal/tui/components/dialog/help.go | 2 +- internal/tui/components/dialog/permission.go | 12 ++-- internal/tui/components/dialog/quit.go | 6 +- internal/tui/components/logs/details.go | 6 +- internal/tui/components/logs/table.go | 10 +-- internal/tui/layout/border.go | 2 +- internal/tui/layout/container.go | 2 +- internal/tui/layout/overlay.go | 4 +- internal/tui/layout/split.go | 2 +- internal/tui/page/chat.go | 10 +-- internal/tui/page/logs.go | 4 +- internal/tui/tui.go | 18 +++--- main.go | 4 +- 66 files changed, 266 insertions(+), 266 deletions(-) (limited to 'internal/llm/tools/write_test.go') diff --git a/.opencode.json b/.opencode.json index b7fc19b52..4b2944f86 100644 --- a/.opencode.json +++ b/.opencode.json @@ -3,5 +3,16 @@ "gopls": { "command": "gopls" } + }, + "agents": { + "coder": { + "model": "gpt-4.1" + }, + "task": { + "model": "gpt-4.1" + }, + "title": { + "model": "gpt-4.1" + } } } diff --git a/cmd/root.go b/cmd/root.go index ff71747d5..f506e9940 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,13 +8,13 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/llm/agent" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/tui" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/llm/agent" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/tui" zone "github.com/lrstanley/bubblezone" "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index 16c88d3a6..822e70dbd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/kujtimiihoxha/termai +module github.com/kujtimiihoxha/opencode go 1.24.0 diff --git a/internal/app/app.go b/internal/app/app.go index 1c16ccc11..748fdaa7f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,15 +7,15 @@ import ( "sync" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/llm/agent" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/llm/agent" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/session" ) type App struct { diff --git a/internal/app/lsp.go b/internal/app/lsp.go index 4a762f1a1..d8a35c8b3 100644 --- a/internal/app/lsp.go +++ b/internal/app/lsp.go @@ -4,10 +4,10 @@ import ( "context" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/watcher" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/watcher" ) func (app *App) initLSPClients(ctx context.Context) { diff --git a/internal/config/config.go b/internal/config/config.go index 147d6c83a..20a8bac97 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,8 +7,8 @@ import ( "os" "strings" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/logging" "github.com/spf13/viper" ) diff --git a/internal/db/connect.go b/internal/db/connect.go index 8bba9cad8..e850bc8d0 100644 --- a/internal/db/connect.go +++ b/internal/db/connect.go @@ -12,8 +12,8 @@ import ( "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/mattn/go-sqlite3" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" ) func Connect() (*sql.DB, error) { @@ -24,7 +24,7 @@ func Connect() (*sql.DB, error) { if err := os.MkdirAll(dataDir, 0o700); err != nil { return nil, fmt.Errorf("failed to create data directory: %w", err) } - dbPath := filepath.Join(dataDir, "termai.db") + dbPath := filepath.Join(dataDir, "opencode.db") // Open the SQLite database db, err := sql.Open("sqlite3", dbPath) if err != nil { diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 829554c7e..f48079c9c 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -19,8 +19,8 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" "github.com/sergi/go-diff/diffmatchpatch" ) diff --git a/internal/history/file.go b/internal/history/file.go index 82017d4cf..1e8bc50bb 100644 --- a/internal/history/file.go +++ b/internal/history/file.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) const ( diff --git a/internal/llm/agent/agent-tool.go b/internal/llm/agent/agent-tool.go index 308412bde..be6e09a9b 100644 --- a/internal/llm/agent/agent-tool.go +++ b/internal/llm/agent/agent-tool.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/session" ) type agentTool struct { diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index ab2742ec1..a5dadb89d 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -7,15 +7,15 @@ import ( "strings" "sync" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/prompt" - "github.com/kujtimiihoxha/termai/internal/llm/provider" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/prompt" + "github.com/kujtimiihoxha/opencode/internal/llm/provider" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/session" ) // Common errors diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index c7ea4916c..16dddc1ba 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/version" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/version" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/mcp" diff --git a/internal/llm/agent/tools.go b/internal/llm/agent/tools.go index a37f1d65d..409d14273 100644 --- a/internal/llm/agent/tools.go +++ b/internal/llm/agent/tools.go @@ -3,12 +3,12 @@ package agent import ( "context" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/session" ) func CoderAgentTools( diff --git a/internal/llm/prompt/coder.go b/internal/llm/prompt/coder.go index 7439fd570..3a06911da 100644 --- a/internal/llm/prompt/coder.go +++ b/internal/llm/prompt/coder.go @@ -8,9 +8,9 @@ import ( "runtime" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" ) func CoderPrompt(provider models.ModelProvider) string { @@ -24,69 +24,58 @@ func CoderPrompt(provider models.ModelProvider) string { return fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation()) } -const baseOpenAICoderPrompt = `You are termAI, an autonomous CLI-based software engineer. Your job is to reduce user effort by proactively reasoning, inferring context, and solving software engineering tasks end-to-end with minimal prompting. - -# Your mindset -Act like a competent, efficient software engineer who is familiar with large codebases. You should: -- Think critically about user requests. -- Proactively search the codebase for related information. -- Infer likely commands, tools, or conventions. -- Write and edit code with minimal user input. -- Anticipate next steps (tests, lints, etc.), but never commit unless explicitly told. - -# Context awareness -- Before acting, infer the purpose of a file from its name, directory, and neighboring files. -- If a file or function appears malicious, refuse to interact with it or discuss it. -- If a termai.md file exists, auto-load it as memory. Offer to update it only if new useful info appears (commands, preferences, structure). - -# CLI communication -- Use GitHub-flavored markdown in monospace font. -- Be concise. Never add preambles or postambles unless asked. Max 4 lines per response. -- Never explain your code unless asked. Do not narrate actions. -- Avoid unnecessary questions. Infer, search, act. - -# Behavior guidelines -- Follow project conventions: naming, formatting, libraries, frameworks. -- Before using any library or framework, confirm it’s already used. -- Always look at the surrounding code to match existing style. -- Do not add comments unless the code is complex or the user asks. - -# Autonomy rules -You are allowed and expected to: -- Search for commands, tools, or config files before asking the user. -- Run multiple search tool calls concurrently to gather relevant context. -- Choose test, lint, and typecheck commands based on package files or scripts. -- Offer to store these commands in termai.md if not already present. - -# Example behavior -user: write tests for new feature -assistant: [searches for existing test patterns, finds appropriate location, generates test code using existing style, optionally asks to add test command to termai.md] +const baseOpenAICoderPrompt = ` +You are **OpenCode**, an autonomous CLI assistant for software‑engineering tasks. + +### ── INTERNAL REFLECTION ── +• Silently think step‑by‑step about the user request, directory layout, and tool calls (never reveal this). +• Formulate a plan, then execute without further approval unless a blocker triggers the Ask‑Only‑If rules. + +### ── PUBLIC RESPONSE RULES ── +• Visible reply ≤ 4 lines; no fluff, preamble, or postamble. +• Use GitHub‑flavored Markdown. +• When running a non‑trivial shell command, add ≤ 1 brief purpose sentence. + +### ── CONTEXT & MEMORY ── +• Infer file intent from directory structure before editing. +• Auto‑load 'OpenCode.md'; ask once before writing new reusable commands or style notes. -user: how do I typecheck this codebase? -assistant: [searches for known commands, infers package manager, checks for scripts or config files] -tsc --noEmit +### ── AUTONOMY PRIORITY ── +**Ask‑Only‑If Decision Tree:** +1. **Safety risk?** (e.g., destructive command, secret exposure) → ask. +2. **Critical unknown?** (no docs/tests; cannot infer) → ask. +3. **Tool failure after two self‑attempts?** → ask. +Otherwise, proceed autonomously. -user: is X function used anywhere else? -assistant: [searches repo for references, returns file paths and lines] +### ── SAFETY & STYLE ── +• Mimic existing code style; verify libraries exist before import. +• Never commit unless explicitly told. +• After edits, run lint & type‑check (ask for commands once, then offer to store in 'OpenCode.md'). +• Protect secrets; follow standard security practices :contentReference[oaicite:2]{index=2}. -# Tool usage -- Use parallel calls when possible. -- Use file search and content tools before asking the user. -- Do not ask the user for information unless it cannot be determined via tools. +### ── TOOL USAGE ── +• Batch independent Agent search/file calls in one block for efficiency :contentReference[oaicite:3]{index=3}. +• Communicate with the user only via visible text; do not expose tool output or internal reasoning. -Never commit changes unless the user explicitly asks you to.` +### ── EXAMPLES ── +user: list files +assistant: ls + +user: write tests for new feature +assistant: [searches & edits autonomously, no extra chit‑chat] +` -const baseAnthropicCoderPrompt = `You are termAI, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. +const baseAnthropicCoderPrompt = `You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. # Memory -If the current working directory contains a file called termai.md, it will be automatically added to your context. This file serves multiple purposes: +If the current working directory contains a file called OpenCode.md, it will be automatically added to your context. This file serves multiple purposes: 1. Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time 2. Recording the user's code style preferences (naming conventions, preferred libraries, etc.) 3. Maintaining useful information about the codebase structure and organization -When you spend time searching for commands to typecheck, lint, build, or test, you should ask the user if it's okay to add those commands to termai.md. Similarly, when learning about code style preferences or important codebase information, ask if it's okay to add that to termai.md so you can remember it for next time. +When you spend time searching for commands to typecheck, lint, build, or test, you should ask the user if it's okay to add those commands to OpenCode.md. Similarly, when learning about code style preferences or important codebase information, ask if it's okay to add that to OpenCode.md so you can remember it for next time. # Tone and style You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system). @@ -161,7 +150,7 @@ The user will primarily request you perform software engineering tasks. This inc 1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially. 2. Implement the solution using all tools available to you 3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. -4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to termai.md so that you will know to run it next time. +4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time. NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. diff --git a/internal/llm/prompt/prompt.go b/internal/llm/prompt/prompt.go index 63fc2df7b..cdc3560ce 100644 --- a/internal/llm/prompt/prompt.go +++ b/internal/llm/prompt/prompt.go @@ -1,8 +1,8 @@ package prompt import ( - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" ) func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string { diff --git a/internal/llm/prompt/task.go b/internal/llm/prompt/task.go index 8bf604ad9..88cd1a0f4 100644 --- a/internal/llm/prompt/task.go +++ b/internal/llm/prompt/task.go @@ -3,11 +3,11 @@ package prompt import ( "fmt" - "github.com/kujtimiihoxha/termai/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/models" ) func TaskPrompt(_ models.ModelProvider) string { - agentPrompt := `You are an agent for termAI. Given the user's prompt, you should use the tools available to you to answer the user's question. + agentPrompt := `You are an agent for OpenCode. Given the user's prompt, you should use the tools available to you to answer the user's question. Notes: 1. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is .", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". 2. When relevant, share file names and code snippets relevant to the query diff --git a/internal/llm/prompt/title.go b/internal/llm/prompt/title.go index 3023a8550..6e5289b24 100644 --- a/internal/llm/prompt/title.go +++ b/internal/llm/prompt/title.go @@ -1,6 +1,6 @@ package prompt -import "github.com/kujtimiihoxha/termai/internal/llm/models" +import "github.com/kujtimiihoxha/opencode/internal/llm/models" func TitlePrompt(_ models.ModelProvider) string { return `you will generate a short title based on the first message a user begins a conversation with diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index c3a4efc49..7bbc02103 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -12,10 +12,10 @@ import ( "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/bedrock" "github.com/anthropics/anthropic-sdk-go/option" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" ) type anthropicOptions struct { diff --git a/internal/llm/provider/bedrock.go b/internal/llm/provider/bedrock.go index d76925ad1..9415b30fe 100644 --- a/internal/llm/provider/bedrock.go +++ b/internal/llm/provider/bedrock.go @@ -7,8 +7,8 @@ import ( "os" "strings" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/message" ) type bedrockOptions struct { diff --git a/internal/llm/provider/gemini.go b/internal/llm/provider/gemini.go index 804baea28..384bff900 100644 --- a/internal/llm/provider/gemini.go +++ b/internal/llm/provider/gemini.go @@ -11,10 +11,10 @@ import ( "github.com/google/generative-ai-go/genai" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" "google.golang.org/api/iterator" "google.golang.org/api/option" ) diff --git a/internal/llm/provider/openai.go b/internal/llm/provider/openai.go index 9c2ad2012..13ce934f2 100644 --- a/internal/llm/provider/openai.go +++ b/internal/llm/provider/openai.go @@ -8,10 +8,10 @@ import ( "io" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" "github.com/openai/openai-go" "github.com/openai/openai-go/option" ) diff --git a/internal/llm/provider/provider.go b/internal/llm/provider/provider.go index 1a5b3dc8a..e04bee71b 100644 --- a/internal/llm/provider/provider.go +++ b/internal/llm/provider/provider.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/message" ) type EventType string diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index c7c970e5a..18533b761 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools/shell" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools/shell" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type BashParams struct { @@ -122,16 +122,16 @@ When the user asks you to create a new git commit, follow these steps carefully: 4. Create the commit with a message ending with: -🤖 Generated with termai -Co-Authored-By: termai +🤖 Generated with opencode +Co-Authored-By: opencode - In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example: git commit -m "$(cat <<'EOF' Commit message here. - 🤖 Generated with termai - Co-Authored-By: termai + 🤖 Generated with opencode + Co-Authored-By: opencode EOF )" @@ -193,7 +193,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF' ## Test plan [Checklist of TODOs for testing the pull request...] -🤖 Generated with termai +🤖 Generated with opencode EOF )" diff --git a/internal/llm/tools/diagnostics.go b/internal/llm/tools/diagnostics.go index b7b2bb8ba..82989c774 100644 --- a/internal/llm/tools/diagnostics.go +++ b/internal/llm/tools/diagnostics.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) type DiagnosticsParams struct { diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go index 148e7aba7..6a1616010 100644 --- a/internal/llm/tools/edit.go +++ b/internal/llm/tools/edit.go @@ -9,11 +9,11 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type EditParams struct { diff --git a/internal/llm/tools/edit_test.go b/internal/llm/tools/edit_test.go index 0971775dd..1b58a0d7d 100644 --- a/internal/llm/tools/edit_test.go +++ b/internal/llm/tools/edit_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/internal/llm/tools/fetch.go b/internal/llm/tools/fetch.go index 91bcb36a0..827755863 100644 --- a/internal/llm/tools/fetch.go +++ b/internal/llm/tools/fetch.go @@ -11,8 +11,8 @@ import ( md "github.com/JohannesKaufmann/html-to-markdown" "github.com/PuerkitoBio/goquery" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type FetchParams struct { @@ -146,7 +146,7 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error return ToolResponse{}, fmt.Errorf("failed to create request: %w", err) } - req.Header.Set("User-Agent", "termai/1.0") + req.Header.Set("User-Agent", "opencode/1.0") resp, err := client.Do(req) if err != nil { diff --git a/internal/llm/tools/glob.go b/internal/llm/tools/glob.go index 7b4fb1187..40262ce2b 100644 --- a/internal/llm/tools/glob.go +++ b/internal/llm/tools/glob.go @@ -12,7 +12,7 @@ import ( "time" "github.com/bmatcuk/doublestar/v4" - "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/opencode/internal/config" ) const ( diff --git a/internal/llm/tools/grep.go b/internal/llm/tools/grep.go index 19333f50b..3436dd7eb 100644 --- a/internal/llm/tools/grep.go +++ b/internal/llm/tools/grep.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/opencode/internal/config" ) type GrepParams struct { diff --git a/internal/llm/tools/ls.go b/internal/llm/tools/ls.go index a63bf0eeb..05f300c0e 100644 --- a/internal/llm/tools/ls.go +++ b/internal/llm/tools/ls.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/opencode/internal/config" ) type LSParams struct { diff --git a/internal/llm/tools/mocks_test.go b/internal/llm/tools/mocks_test.go index 321f09ac1..81993160c 100644 --- a/internal/llm/tools/mocks_test.go +++ b/internal/llm/tools/mocks_test.go @@ -9,9 +9,9 @@ import ( "time" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) // Mock permission service for testing diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index 4a776478a..e25bdf3ea 100644 --- a/internal/llm/tools/shell/shell.go +++ b/internal/llm/tools/shell/shell.go @@ -126,10 +126,10 @@ func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx } tempDir := os.TempDir() - stdoutFile := filepath.Join(tempDir, fmt.Sprintf("termai-stdout-%d", time.Now().UnixNano())) - stderrFile := filepath.Join(tempDir, fmt.Sprintf("termai-stderr-%d", time.Now().UnixNano())) - statusFile := filepath.Join(tempDir, fmt.Sprintf("termai-status-%d", time.Now().UnixNano())) - cwdFile := filepath.Join(tempDir, fmt.Sprintf("termai-cwd-%d", time.Now().UnixNano())) + stdoutFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stdout-%d", time.Now().UnixNano())) + stderrFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stderr-%d", time.Now().UnixNano())) + statusFile := filepath.Join(tempDir, fmt.Sprintf("opencode-status-%d", time.Now().UnixNano())) + cwdFile := filepath.Join(tempDir, fmt.Sprintf("opencode-cwd-%d", time.Now().UnixNano())) defer func() { os.Remove(stdoutFile) diff --git a/internal/llm/tools/sourcegraph.go b/internal/llm/tools/sourcegraph.go index a6f2c8afb..0d38c975f 100644 --- a/internal/llm/tools/sourcegraph.go +++ b/internal/llm/tools/sourcegraph.go @@ -218,7 +218,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse, } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "termai/1.0") + req.Header.Set("User-Agent", "opencode/1.0") resp, err := client.Do(req) if err != nil { diff --git a/internal/llm/tools/view.go b/internal/llm/tools/view.go index 7450a84bf..3fa4ca116 100644 --- a/internal/llm/tools/view.go +++ b/internal/llm/tools/view.go @@ -10,8 +10,8 @@ import ( "path/filepath" "strings" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/lsp" ) type ViewParams struct { diff --git a/internal/llm/tools/write.go b/internal/llm/tools/write.go index bb49381fd..261865c39 100644 --- a/internal/llm/tools/write.go +++ b/internal/llm/tools/write.go @@ -8,11 +8,11 @@ import ( "path/filepath" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type WriteParams struct { diff --git a/internal/llm/tools/write_test.go b/internal/llm/tools/write_test.go index 2264f36fb..b5ecb3fda 100644 --- a/internal/llm/tools/write_test.go +++ b/internal/llm/tools/write_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/internal/logging/writer.go b/internal/logging/writer.go index 9fe469c5e..1dc07e853 100644 --- a/internal/logging/writer.go +++ b/internal/logging/writer.go @@ -9,7 +9,7 @@ import ( "time" "github.com/go-logfmt/logfmt" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) const ( diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 0f03e7fcb..dad07f3c0 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -13,9 +13,9 @@ import ( "sync/atomic" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) type Client struct { diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index c3088d685..7a11286e6 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -3,10 +3,10 @@ package lsp import ( "encoding/json" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" - "github.com/kujtimiihoxha/termai/internal/lsp/util" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/util" ) // Requests diff --git a/internal/lsp/language.go b/internal/lsp/language.go index 2e276c464..65ccd54f3 100644 --- a/internal/lsp/language.go +++ b/internal/lsp/language.go @@ -4,7 +4,7 @@ import ( "path/filepath" "strings" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) func DetectLanguageID(uri string) protocol.LanguageKind { diff --git a/internal/lsp/methods.go b/internal/lsp/methods.go index 079b3bfe3..ab33d7e1b 100644 --- a/internal/lsp/methods.go +++ b/internal/lsp/methods.go @@ -4,7 +4,7 @@ package lsp import ( "context" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) // Implementation sends a textDocument/implementation request to the LSP server. diff --git a/internal/lsp/transport.go b/internal/lsp/transport.go index 89255fd78..fe59b0fbb 100644 --- a/internal/lsp/transport.go +++ b/internal/lsp/transport.go @@ -8,8 +8,8 @@ import ( "io" "strings" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" ) // Write writes an LSP message to the given writer diff --git a/internal/lsp/util/edit.go b/internal/lsp/util/edit.go index 3b94fb39f..52f03ee77 100644 --- a/internal/lsp/util/edit.go +++ b/internal/lsp/util/edit.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error { diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index 156f38e1a..595c78db9 100644 --- a/internal/lsp/watcher/watcher.go +++ b/internal/lsp/watcher/watcher.go @@ -10,10 +10,10 @@ import ( "time" "github.com/fsnotify/fsnotify" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) // WorkspaceWatcher manages LSP file watching diff --git a/internal/message/content.go b/internal/message/content.go index f9e76b11c..f52449f4a 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -5,7 +5,7 @@ import ( "slices" "time" - "github.com/kujtimiihoxha/termai/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/models" ) type MessageRole string diff --git a/internal/message/message.go b/internal/message/message.go index 2871780a7..f165fcfc7 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -7,9 +7,9 @@ import ( "fmt" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) type CreateMessageParams struct { diff --git a/internal/permission/permission.go b/internal/permission/permission.go index 8aa280906..4cb379dea 100644 --- a/internal/permission/permission.go +++ b/internal/permission/permission.go @@ -6,7 +6,7 @@ import ( "time" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) var ErrorPermissionDenied = errors.New("permission denied") diff --git a/internal/session/session.go b/internal/session/session.go index 019019df4..280da1ff0 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -5,8 +5,8 @@ import ( "database/sql" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) type Session struct { diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index e98001efa..52ff4c8bf 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -5,10 +5,10 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/version" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/version" ) type SendMsg struct { diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index e2f4da9e2..4d6ef5ca0 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -5,11 +5,11 @@ import ( "github.com/charmbracelet/bubbles/textarea" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type editorCmp struct { diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index 26a98970e..c2ce7d88b 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -15,17 +15,17 @@ import ( "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/llm/agent" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/llm/agent" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type uiMessageType int diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index b90269d1a..54b39f4a1 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -7,12 +7,12 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type sidebarCmp struct { diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 089dffa2c..411cac1c5 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -7,12 +7,12 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type statusCmp struct { diff --git a/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go index 1d3c2b077..6242017f1 100644 --- a/internal/tui/components/dialog/help.go +++ b/internal/tui/components/dialog/help.go @@ -6,7 +6,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type helpCmp struct { diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index 9c55effde..200a7970d 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -9,12 +9,12 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type PermissionAction string diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go index 10d9ba8a2..5bbe6696c 100644 --- a/internal/tui/components/dialog/quit.go +++ b/internal/tui/components/dialog/quit.go @@ -6,9 +6,9 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) const question = "Are you sure you want to quit?" diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go index 18eb1a526..3a8f17999 100644 --- a/internal/tui/components/logs/details.go +++ b/internal/tui/components/logs/details.go @@ -9,9 +9,9 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type DetailComponent interface { diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index 6e8eb58b1..dc6184e3d 100644 --- a/internal/tui/components/logs/table.go +++ b/internal/tui/components/logs/table.go @@ -7,11 +7,11 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type TableComponent interface { diff --git a/internal/tui/layout/border.go b/internal/tui/layout/border.go index 8fe5c430c..ea9f5e0bc 100644 --- a/internal/tui/layout/border.go +++ b/internal/tui/layout/border.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type BorderPosition int diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go index db07d49fb..603699955 100644 --- a/internal/tui/layout/container.go +++ b/internal/tui/layout/container.go @@ -4,7 +4,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type Container interface { diff --git a/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go index 4a1bcf661..4c05e8462 100644 --- a/internal/tui/layout/overlay.go +++ b/internal/tui/layout/overlay.go @@ -5,8 +5,8 @@ import ( "strings" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" "github.com/mattn/go-runewidth" "github.com/muesli/ansi" "github.com/muesli/reflow/truncate" diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go index 6482fc74c..bfb616a53 100644 --- a/internal/tui/layout/split.go +++ b/internal/tui/layout/split.go @@ -4,7 +4,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type SplitPaneLayout interface { diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index cebc0e461..c268e677f 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -5,11 +5,11 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/components/chat" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/components/chat" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) var ChatPage PageID = "chat" diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go index d1e557eab..c77a033f4 100644 --- a/internal/tui/page/logs.go +++ b/internal/tui/page/logs.go @@ -2,8 +2,8 @@ package page import ( tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/tui/components/logs" - "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/components/logs" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" ) var LogsPage PageID = "logs" diff --git a/internal/tui/tui.go b/internal/tui/tui.go index dff7ad63d..657de6b6e 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -4,15 +4,15 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/tui/components/core" - "github.com/kujtimiihoxha/termai/internal/tui/components/dialog" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/page" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/tui/components/core" + "github.com/kujtimiihoxha/opencode/internal/tui/components/dialog" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/page" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type keyMap struct { diff --git a/main.go b/main.go index 2e6954646..06578c7ef 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/kujtimiihoxha/termai/cmd" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/cmd" + "github.com/kujtimiihoxha/opencode/internal/logging" ) func main() { -- cgit v1.2.3 From 36172979b45facc8ccec6861f124193eaebc42e9 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 17 Apr 2025 00:00:19 +0200 Subject: Update agent prompt, improve TUI patch UI, remove obsolete tool tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace and expand agent coder prompt for clarity and safety - Add patch tool and TUI dialog support for patch diffs - Sort sidebar modified files by name - Remove Bash/Edit/Sourcegraph/Write tool tests 🤖 Generated with opencode Co-Authored-By: opencode --- internal/llm/agent/tools.go | 2 + internal/llm/prompt/coder.go | 95 +++--- internal/llm/tools/bash_test.go | 340 -------------------- internal/llm/tools/edit_test.go | 461 --------------------------- internal/llm/tools/mocks_test.go | 246 -------------- internal/llm/tools/patch.go | 300 +++++++++++++++++ internal/llm/tools/sourcegraph_test.go | 86 ----- internal/llm/tools/write_test.go | 307 ------------------ internal/tui/components/chat/sidebar.go | 12 +- internal/tui/components/dialog/permission.go | 14 + main.go | 4 +- 11 files changed, 385 insertions(+), 1482 deletions(-) delete mode 100644 internal/llm/tools/bash_test.go delete mode 100644 internal/llm/tools/edit_test.go delete mode 100644 internal/llm/tools/mocks_test.go create mode 100644 internal/llm/tools/patch.go delete mode 100644 internal/llm/tools/sourcegraph_test.go delete mode 100644 internal/llm/tools/write_test.go (limited to 'internal/llm/tools/write_test.go') diff --git a/internal/llm/agent/tools.go b/internal/llm/agent/tools.go index 409d14273..9120809ff 100644 --- a/internal/llm/agent/tools.go +++ b/internal/llm/agent/tools.go @@ -31,6 +31,8 @@ func CoderAgentTools( tools.NewGlobTool(), tools.NewGrepTool(), tools.NewLsTool(), + // TODO: see if we want to use this tool + // tools.NewPatchTool(lspClients, permissions, history), tools.NewSourcegraphTool(), tools.NewViewTool(lspClients), tools.NewWriteTool(lspClients, permissions, history), diff --git a/internal/llm/prompt/coder.go b/internal/llm/prompt/coder.go index 3a06911da..febdea4d2 100644 --- a/internal/llm/prompt/coder.go +++ b/internal/llm/prompt/coder.go @@ -25,44 +25,63 @@ func CoderPrompt(provider models.ModelProvider) string { } const baseOpenAICoderPrompt = ` -You are **OpenCode**, an autonomous CLI assistant for software‑engineering tasks. - -### ── INTERNAL REFLECTION ── -• Silently think step‑by‑step about the user request, directory layout, and tool calls (never reveal this). -• Formulate a plan, then execute without further approval unless a blocker triggers the Ask‑Only‑If rules. - -### ── PUBLIC RESPONSE RULES ── -• Visible reply ≤ 4 lines; no fluff, preamble, or postamble. -• Use GitHub‑flavored Markdown. -• When running a non‑trivial shell command, add ≤ 1 brief purpose sentence. - -### ── CONTEXT & MEMORY ── -• Infer file intent from directory structure before editing. -• Auto‑load 'OpenCode.md'; ask once before writing new reusable commands or style notes. - -### ── AUTONOMY PRIORITY ── -**Ask‑Only‑If Decision Tree:** -1. **Safety risk?** (e.g., destructive command, secret exposure) → ask. -2. **Critical unknown?** (no docs/tests; cannot infer) → ask. -3. **Tool failure after two self‑attempts?** → ask. -Otherwise, proceed autonomously. - -### ── SAFETY & STYLE ── -• Mimic existing code style; verify libraries exist before import. -• Never commit unless explicitly told. -• After edits, run lint & type‑check (ask for commands once, then offer to store in 'OpenCode.md'). -• Protect secrets; follow standard security practices :contentReference[oaicite:2]{index=2}. - -### ── TOOL USAGE ── -• Batch independent Agent search/file calls in one block for efficiency :contentReference[oaicite:3]{index=3}. -• Communicate with the user only via visible text; do not expose tool output or internal reasoning. - -### ── EXAMPLES ── -user: list files -assistant: ls - -user: write tests for new feature -assistant: [searches & edits autonomously, no extra chit‑chat] +# OpenCode CLI Agent Prompt + +You are operating within the **OpenCode CLI**, a terminal-based, agentic coding assistant that interfaces with local codebases through natural language. Your primary objectives are to be precise, safe, and helpful. + +## Capabilities + +- Receive user prompts, project context, and files. +- Stream responses and emit function calls (e.g., shell commands, code edits). +- Apply patches, run commands, and manage user approvals based on policy. +- Operate within a sandboxed, git-backed workspace with rollback support. +- Log telemetry for session replay or inspection. +- Access detailed functionality via the help command. + +## Operational Guidelines + +### 1. Task Resolution + +- Continue processing until the user's query is fully resolved. +- Only conclude your turn when confident the problem is solved. +- If uncertain about file content or codebase structure, utilize available tools to gather necessary information—avoid assumptions. + +### 2. Code Modification & Testing + +- Edit and test code files within your current execution session. +- Work on the local repositories, even if proprietary. +- Analyze code for vulnerabilities when applicable. +- Display user code and tool call details transparently. + +### 3. Coding Guidelines + +- Address root causes rather than applying superficial fixes. +- Avoid unnecessary complexity; focus on the task at hand. +- Update documentation as needed. +- Maintain consistency with the existing codebase style. +- Utilize version control tools for additional context; note that internet access is disabled. +- Refrain from adding copyright or license headers unless explicitly requested. +- No need to perform commit operations; this will be handled automatically. +- If a pre-commit configuration file exists, run the appropriate checks to ensure changes pass. Do not fix pre-existing errors on untouched lines. +- If pre-commit checks fail after retries, inform the user that the setup may be broken. + +### 4. Post-Modification Checks + +- Use version control status commands to verify changes; revert any unintended modifications. +- Remove all added inline comments unless they are essential for understanding. +- Ensure no accidental addition of copyright or license headers. +- Attempt to run pre-commit checks if available. +- For smaller tasks, provide brief bullet points summarizing changes. +- For complex tasks, include a high-level description, bullet points, and relevant details for code reviewers. + +### 5. Non-Code Modification Tasks + +- Respond in a friendly, collaborative tone, akin to a knowledgeable remote teammate eager to assist with coding inquiries. + +### 6. File Handling + +- Do not instruct the user to save or copy code into files if modifications have already been made using the editing tools. +- Avoid displaying full contents of large files unless explicitly requested by the user. ` const baseAnthropicCoderPrompt = `You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. diff --git a/internal/llm/tools/bash_test.go b/internal/llm/tools/bash_test.go deleted file mode 100644 index dafb0ccc5..000000000 --- a/internal/llm/tools/bash_test.go +++ /dev/null @@ -1,340 +0,0 @@ -package tools - -import ( - "context" - "encoding/json" - "os" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBashTool_Info(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - info := tool.Info() - - assert.Equal(t, BashToolName, info.Name) - assert.NotEmpty(t, info.Description) - assert.Contains(t, info.Parameters, "command") - assert.Contains(t, info.Parameters, "timeout") - assert.Contains(t, info.Required, "command") -} - -func TestBashTool_Run(t *testing.T) { - // Save original working directory - origWd, err := os.Getwd() - require.NoError(t, err) - defer func() { - os.Chdir(origWd) - }() - - t.Run("executes command successfully", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "echo 'Hello World'", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Equal(t, "Hello World\n", response.Content) - }) - - t.Run("handles invalid parameters", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - call := ToolCall{ - Name: BashToolName, - Input: "invalid json", - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "invalid parameters") - }) - - t.Run("handles missing command", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "missing command") - }) - - t.Run("handles banned commands", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - - for _, bannedCmd := range bannedCommands { - params := BashParams{ - Command: bannedCmd + " arg1 arg2", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "not allowed", "Command %s should be blocked", bannedCmd) - } - }) - - t.Run("handles multi-word safe commands without permission check", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(false)) - - // Test with multi-word safe commands - multiWordCommands := []string{ - "go env", - } - - for _, cmd := range multiWordCommands { - params := BashParams{ - Command: cmd, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.NotContains(t, response.Content, "permission denied", - "Command %s should be allowed without permission", cmd) - } - }) - - t.Run("handles permission denied", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(false)) - - // Test with a command that requires permission - params := BashParams{ - Command: "mkdir test_dir", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "permission denied") - }) - - t.Run("handles command timeout", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "sleep 2", - Timeout: 100, // 100ms timeout - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "aborted") - }) - - t.Run("handles command with stderr output", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "echo 'error message' >&2", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "error message") - }) - - t.Run("handles command with both stdout and stderr", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "echo 'stdout message' && echo 'stderr message' >&2", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "stdout message") - assert.Contains(t, response.Content, "stderr message") - }) - - t.Run("handles context cancellation", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "sleep 5", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - ctx, cancel := context.WithCancel(context.Background()) - - // Cancel the context after a short delay - go func() { - time.Sleep(100 * time.Millisecond) - cancel() - }() - - response, err := tool.Run(ctx, call) - require.NoError(t, err) - assert.Contains(t, response.Content, "aborted") - }) - - t.Run("respects max timeout", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "echo 'test'", - Timeout: MaxTimeout + 1000, // Exceeds max timeout - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Equal(t, "test\n", response.Content) - }) - - t.Run("uses default timeout for zero or negative timeout", func(t *testing.T) { - tool := NewBashTool(newMockPermissionService(true)) - params := BashParams{ - Command: "echo 'test'", - Timeout: -100, // Negative timeout - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: BashToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Equal(t, "test\n", response.Content) - }) -} - -func TestTruncateOutput(t *testing.T) { - t.Run("does not truncate short output", func(t *testing.T) { - output := "short output" - result := truncateOutput(output) - assert.Equal(t, output, result) - }) - - t.Run("truncates long output", func(t *testing.T) { - // Create a string longer than MaxOutputLength - longOutput := strings.Repeat("a\n", MaxOutputLength) - result := truncateOutput(longOutput) - - // Check that the result is shorter than the original - assert.Less(t, len(result), len(longOutput)) - - // Check that the truncation message is included - assert.Contains(t, result, "lines truncated") - - // Check that we have the beginning and end of the original string - assert.True(t, strings.HasPrefix(result, "a\n")) - assert.True(t, strings.HasSuffix(result, "a\n")) - }) -} - -func TestCountLines(t *testing.T) { - testCases := []struct { - name string - input string - expected int - }{ - { - name: "empty string", - input: "", - expected: 0, - }, - { - name: "single line", - input: "line1", - expected: 1, - }, - { - name: "multiple lines", - input: "line1\nline2\nline3", - expected: 3, - }, - { - name: "trailing newline", - input: "line1\nline2\n", - expected: 3, // Empty string after last newline counts as a line - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := countLines(tc.input) - assert.Equal(t, tc.expected, result) - }) - } -} diff --git a/internal/llm/tools/edit_test.go b/internal/llm/tools/edit_test.go deleted file mode 100644 index 1b58a0d7d..000000000 --- a/internal/llm/tools/edit_test.go +++ /dev/null @@ -1,461 +0,0 @@ -package tools - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "testing" - "time" - - "github.com/kujtimiihoxha/opencode/internal/lsp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEditTool_Info(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - info := tool.Info() - - assert.Equal(t, EditToolName, info.Name) - assert.NotEmpty(t, info.Description) - assert.Contains(t, info.Parameters, "file_path") - assert.Contains(t, info.Parameters, "old_string") - assert.Contains(t, info.Parameters, "new_string") - assert.Contains(t, info.Required, "file_path") - assert.Contains(t, info.Required, "old_string") - assert.Contains(t, info.Required, "new_string") -} - -func TestEditTool_Run(t *testing.T) { - // Create a temporary directory for testing - tempDir, err := os.MkdirTemp("", "edit_tool_test") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - - t.Run("creates a new file successfully", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - filePath := filepath.Join(tempDir, "new_file.txt") - content := "This is a test content" - - params := EditParams{ - FilePath: filePath, - OldString: "", - NewString: content, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "File created") - - // Verify file was created with correct content - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, content, string(fileContent)) - }) - - t.Run("creates file with nested directories", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt") - content := "Content in nested directory" - - params := EditParams{ - FilePath: filePath, - OldString: "", - NewString: content, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "File created") - - // Verify file was created with correct content - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, content, string(fileContent)) - }) - - t.Run("fails to create file that already exists", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file first - filePath := filepath.Join(tempDir, "existing_file.txt") - initialContent := "Initial content" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Try to create the same file - params := EditParams{ - FilePath: filePath, - OldString: "", - NewString: "New content", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "file already exists") - }) - - t.Run("fails to create file when path is a directory", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a directory - dirPath := filepath.Join(tempDir, "test_dir") - err := os.Mkdir(dirPath, 0o755) - require.NoError(t, err) - - // Try to create a file with the same path as the directory - params := EditParams{ - FilePath: dirPath, - OldString: "", - NewString: "Some content", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "path is a directory") - }) - - t.Run("replaces content successfully", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file first - filePath := filepath.Join(tempDir, "replace_content.txt") - initialContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record the file read to avoid modification time check failure - recordFileRead(filePath) - - // Replace content - oldString := "Line 2\nLine 3" - newString := "Line 2 modified\nLine 3 modified" - params := EditParams{ - FilePath: filePath, - OldString: oldString, - NewString: newString, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "Content replaced") - - // Verify file was updated with correct content - expectedContent := "Line 1\nLine 2 modified\nLine 3 modified\nLine 4\nLine 5" - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, expectedContent, string(fileContent)) - }) - - t.Run("deletes content successfully", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file first - filePath := filepath.Join(tempDir, "delete_content.txt") - initialContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record the file read to avoid modification time check failure - recordFileRead(filePath) - - // Delete content - oldString := "Line 2\nLine 3\n" - params := EditParams{ - FilePath: filePath, - OldString: oldString, - NewString: "", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "Content deleted") - - // Verify file was updated with correct content - expectedContent := "Line 1\nLine 4\nLine 5" - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, expectedContent, string(fileContent)) - }) - - t.Run("handles invalid parameters", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - call := ToolCall{ - Name: EditToolName, - Input: "invalid json", - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "invalid parameters") - }) - - t.Run("handles missing file_path", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - params := EditParams{ - FilePath: "", - OldString: "old", - NewString: "new", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "file_path is required") - }) - - t.Run("handles file not found", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - filePath := filepath.Join(tempDir, "non_existent_file.txt") - params := EditParams{ - FilePath: filePath, - OldString: "old content", - NewString: "new content", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "file not found") - }) - - t.Run("handles old_string not found in file", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file first - filePath := filepath.Join(tempDir, "content_not_found.txt") - initialContent := "Line 1\nLine 2\nLine 3" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record the file read to avoid modification time check failure - recordFileRead(filePath) - - // Try to replace content that doesn't exist - params := EditParams{ - FilePath: filePath, - OldString: "This content does not exist", - NewString: "new content", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "old_string not found in file") - }) - - t.Run("handles multiple occurrences of old_string", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file with duplicate content - filePath := filepath.Join(tempDir, "duplicate_content.txt") - initialContent := "Line 1\nDuplicate\nLine 3\nDuplicate\nLine 5" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record the file read to avoid modification time check failure - recordFileRead(filePath) - - // Try to replace content that appears multiple times - params := EditParams{ - FilePath: filePath, - OldString: "Duplicate", - NewString: "Replaced", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "appears multiple times") - }) - - t.Run("handles file modified since last read", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file - filePath := filepath.Join(tempDir, "modified_file.txt") - initialContent := "Initial content" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record an old read time - fileRecordMutex.Lock() - fileRecords[filePath] = fileRecord{ - path: filePath, - readTime: time.Now().Add(-1 * time.Hour), - } - fileRecordMutex.Unlock() - - // Try to update the file - params := EditParams{ - FilePath: filePath, - OldString: "Initial", - NewString: "Updated", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "has been modified since it was last read") - - // Verify file was not modified - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, initialContent, string(fileContent)) - }) - - t.Run("handles file not read before editing", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file - filePath := filepath.Join(tempDir, "not_read_file.txt") - initialContent := "Initial content" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Try to update the file without reading it first - params := EditParams{ - FilePath: filePath, - OldString: "Initial", - NewString: "Updated", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "you must read the file before editing it") - }) - - t.Run("handles permission denied", func(t *testing.T) { - tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(false), newMockFileHistoryService()) - - // Create a file - filePath := filepath.Join(tempDir, "permission_denied.txt") - initialContent := "Initial content" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record the file read to avoid modification time check failure - recordFileRead(filePath) - - // Try to update the file - params := EditParams{ - FilePath: filePath, - OldString: "Initial", - NewString: "Updated", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: EditToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "permission denied") - - // Verify file was not modified - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, initialContent, string(fileContent)) - }) -} diff --git a/internal/llm/tools/mocks_test.go b/internal/llm/tools/mocks_test.go deleted file mode 100644 index 81993160c..000000000 --- a/internal/llm/tools/mocks_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package tools - -import ( - "context" - "fmt" - "sort" - "strconv" - "strings" - "time" - - "github.com/google/uuid" - "github.com/kujtimiihoxha/opencode/internal/history" - "github.com/kujtimiihoxha/opencode/internal/permission" - "github.com/kujtimiihoxha/opencode/internal/pubsub" -) - -// Mock permission service for testing -type mockPermissionService struct { - *pubsub.Broker[permission.PermissionRequest] - allow bool -} - -func (m *mockPermissionService) GrantPersistant(permission permission.PermissionRequest) { - // Not needed for tests -} - -func (m *mockPermissionService) Grant(permission permission.PermissionRequest) { - // Not needed for tests -} - -func (m *mockPermissionService) Deny(permission permission.PermissionRequest) { - // Not needed for tests -} - -func (m *mockPermissionService) Request(opts permission.CreatePermissionRequest) bool { - return m.allow -} - -func newMockPermissionService(allow bool) permission.Service { - return &mockPermissionService{ - Broker: pubsub.NewBroker[permission.PermissionRequest](), - allow: allow, - } -} - -type mockFileHistoryService struct { - *pubsub.Broker[history.File] - files map[string]history.File // ID -> File - timeNow func() int64 -} - -// Create implements history.Service. -func (m *mockFileHistoryService) Create(ctx context.Context, sessionID string, path string, content string) (history.File, error) { - return m.createWithVersion(ctx, sessionID, path, content, history.InitialVersion) -} - -// CreateVersion implements history.Service. -func (m *mockFileHistoryService) CreateVersion(ctx context.Context, sessionID string, path string, content string) (history.File, error) { - var files []history.File - for _, file := range m.files { - if file.Path == path { - files = append(files, file) - } - } - - if len(files) == 0 { - // No previous versions, create initial - return m.Create(ctx, sessionID, path, content) - } - - // Sort files by CreatedAt in descending order - sort.Slice(files, func(i, j int) bool { - return files[i].CreatedAt > files[j].CreatedAt - }) - - // Get the latest version - latestFile := files[0] - latestVersion := latestFile.Version - - // Generate the next version - var nextVersion string - if latestVersion == history.InitialVersion { - nextVersion = "v1" - } else if strings.HasPrefix(latestVersion, "v") { - versionNum, err := strconv.Atoi(latestVersion[1:]) - if err != nil { - // If we can't parse the version, just use a timestamp-based version - nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt) - } else { - nextVersion = fmt.Sprintf("v%d", versionNum+1) - } - } else { - // If the version format is unexpected, use a timestamp-based version - nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt) - } - - return m.createWithVersion(ctx, sessionID, path, content, nextVersion) -} - -func (m *mockFileHistoryService) createWithVersion(_ context.Context, sessionID, path, content, version string) (history.File, error) { - now := m.timeNow() - file := history.File{ - ID: uuid.New().String(), - SessionID: sessionID, - Path: path, - Content: content, - Version: version, - CreatedAt: now, - UpdatedAt: now, - } - - m.files[file.ID] = file - m.Publish(pubsub.CreatedEvent, file) - return file, nil -} - -// Delete implements history.Service. -func (m *mockFileHistoryService) Delete(ctx context.Context, id string) error { - file, ok := m.files[id] - if !ok { - return fmt.Errorf("file not found: %s", id) - } - - delete(m.files, id) - m.Publish(pubsub.DeletedEvent, file) - return nil -} - -// DeleteSessionFiles implements history.Service. -func (m *mockFileHistoryService) DeleteSessionFiles(ctx context.Context, sessionID string) error { - files, err := m.ListBySession(ctx, sessionID) - if err != nil { - return err - } - - for _, file := range files { - err = m.Delete(ctx, file.ID) - if err != nil { - return err - } - } - - return nil -} - -// Get implements history.Service. -func (m *mockFileHistoryService) Get(ctx context.Context, id string) (history.File, error) { - file, ok := m.files[id] - if !ok { - return history.File{}, fmt.Errorf("file not found: %s", id) - } - return file, nil -} - -// GetByPathAndSession implements history.Service. -func (m *mockFileHistoryService) GetByPathAndSession(ctx context.Context, path string, sessionID string) (history.File, error) { - var latestFile history.File - var found bool - var latestTime int64 - - for _, file := range m.files { - if file.Path == path && file.SessionID == sessionID { - if !found || file.CreatedAt > latestTime { - latestFile = file - latestTime = file.CreatedAt - found = true - } - } - } - - if !found { - return history.File{}, fmt.Errorf("file not found: %s for session %s", path, sessionID) - } - return latestFile, nil -} - -// ListBySession implements history.Service. -func (m *mockFileHistoryService) ListBySession(ctx context.Context, sessionID string) ([]history.File, error) { - var files []history.File - for _, file := range m.files { - if file.SessionID == sessionID { - files = append(files, file) - } - } - - // Sort by CreatedAt in descending order - sort.Slice(files, func(i, j int) bool { - return files[i].CreatedAt > files[j].CreatedAt - }) - - return files, nil -} - -// ListLatestSessionFiles implements history.Service. -func (m *mockFileHistoryService) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]history.File, error) { - // Map to track the latest file for each path - latestFiles := make(map[string]history.File) - - for _, file := range m.files { - if file.SessionID == sessionID { - existing, ok := latestFiles[file.Path] - if !ok || file.CreatedAt > existing.CreatedAt { - latestFiles[file.Path] = file - } - } - } - - // Convert map to slice - var result []history.File - for _, file := range latestFiles { - result = append(result, file) - } - - // Sort by CreatedAt in descending order - sort.Slice(result, func(i, j int) bool { - return result[i].CreatedAt > result[j].CreatedAt - }) - - return result, nil -} - -// Subscribe implements history.Service. -func (m *mockFileHistoryService) Subscribe(ctx context.Context) <-chan pubsub.Event[history.File] { - return m.Broker.Subscribe(ctx) -} - -// Update implements history.Service. -func (m *mockFileHistoryService) Update(ctx context.Context, file history.File) (history.File, error) { - _, ok := m.files[file.ID] - if !ok { - return history.File{}, fmt.Errorf("file not found: %s", file.ID) - } - - file.UpdatedAt = m.timeNow() - m.files[file.ID] = file - m.Publish(pubsub.UpdatedEvent, file) - return file, nil -} - -func newMockFileHistoryService() history.Service { - return &mockFileHistoryService{ - Broker: pubsub.NewBroker[history.File](), - files: make(map[string]history.File), - timeNow: func() int64 { return time.Now().Unix() }, - } -} diff --git a/internal/llm/tools/patch.go b/internal/llm/tools/patch.go new file mode 100644 index 000000000..12060d72a --- /dev/null +++ b/internal/llm/tools/patch.go @@ -0,0 +1,300 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/permission" +) + +type PatchParams struct { + FilePath string `json:"file_path"` + Patch string `json:"patch"` +} + +type PatchPermissionsParams struct { + FilePath string `json:"file_path"` + Diff string `json:"diff"` +} + +type PatchResponseMetadata struct { + Diff string `json:"diff"` + Additions int `json:"additions"` + Removals int `json:"removals"` +} + +type patchTool struct { + lspClients map[string]*lsp.Client + permissions permission.Service + files history.Service +} + +const ( + // TODO: test if this works as expected + PatchToolName = "patch" + patchDescription = `Applies a patch to a file. This tool is similar to the edit tool but accepts a unified diff patch instead of old/new strings. + +Before using this tool: + +1. Use the FileRead tool to understand the file's contents and context + +2. Verify the directory path is correct: + - Use the LS tool to verify the parent directory exists and is the correct location + +To apply a patch, provide the following: +1. file_path: The absolute path to the file to modify (must be absolute, not relative) +2. patch: A unified diff patch to apply to the file + +The tool will apply the patch to the specified file. The patch must be in unified diff format. + +CRITICAL REQUIREMENTS FOR USING THIS TOOL: + +1. PATCH FORMAT: The patch must be in unified diff format, which includes: + - File headers (--- a/file_path, +++ b/file_path) + - Hunk headers (@@ -start,count +start,count @@) + - Added lines (prefixed with +) + - Removed lines (prefixed with -) + +2. CONTEXT: The patch must include sufficient context around the changes to ensure it applies correctly. + +3. VERIFICATION: Before using this tool: + - Ensure the patch applies cleanly to the current state of the file + - Check that the file exists and you have read it first + +WARNING: If you do not follow these requirements: + - The tool will fail if the patch doesn't apply cleanly + - You may change the wrong parts of the file if the context is insufficient + +When applying patches: + - Ensure the patch results in idiomatic, correct code + - Do not leave the code in a broken state + - Always use absolute file paths (starting with /) + +Remember: patches are a powerful way to make multiple related changes at once, but they require careful preparation.` +) + +func NewPatchTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service) BaseTool { + return &patchTool{ + lspClients: lspClients, + permissions: permissions, + files: files, + } +} + +func (p *patchTool) Info() ToolInfo { + return ToolInfo{ + Name: PatchToolName, + Description: patchDescription, + Parameters: map[string]any{ + "file_path": map[string]any{ + "type": "string", + "description": "The absolute path to the file to modify", + }, + "patch": map[string]any{ + "type": "string", + "description": "The unified diff patch to apply", + }, + }, + Required: []string{"file_path", "patch"}, + } +} + +func (p *patchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { + var params PatchParams + if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { + return NewTextErrorResponse("invalid parameters"), nil + } + + if params.FilePath == "" { + return NewTextErrorResponse("file_path is required"), nil + } + + if params.Patch == "" { + return NewTextErrorResponse("patch is required"), nil + } + + if !filepath.IsAbs(params.FilePath) { + wd := config.WorkingDirectory() + params.FilePath = filepath.Join(wd, params.FilePath) + } + + // Check if file exists + fileInfo, err := os.Stat(params.FilePath) + if err != nil { + if os.IsNotExist(err) { + return NewTextErrorResponse(fmt.Sprintf("file not found: %s", params.FilePath)), nil + } + return ToolResponse{}, fmt.Errorf("failed to access file: %w", err) + } + + if fileInfo.IsDir() { + return NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", params.FilePath)), nil + } + + if getLastReadTime(params.FilePath).IsZero() { + return NewTextErrorResponse("you must read the file before patching it. Use the View tool first"), nil + } + + modTime := fileInfo.ModTime() + lastRead := getLastReadTime(params.FilePath) + if modTime.After(lastRead) { + return NewTextErrorResponse( + fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)", + params.FilePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339), + )), nil + } + + // Read the current file content + content, err := os.ReadFile(params.FilePath) + if err != nil { + return ToolResponse{}, fmt.Errorf("failed to read file: %w", err) + } + + oldContent := string(content) + + // Parse and apply the patch + diffResult, err := diff.ParseUnifiedDiff(params.Patch) + if err != nil { + return NewTextErrorResponse(fmt.Sprintf("failed to parse patch: %v", err)), nil + } + + // Apply the patch to get the new content + newContent, err := applyPatch(oldContent, diffResult) + if err != nil { + return NewTextErrorResponse(fmt.Sprintf("failed to apply patch: %v", err)), nil + } + + if oldContent == newContent { + return NewTextErrorResponse("patch did not result in any changes to the file"), nil + } + + sessionID, messageID := GetContextValues(ctx) + if sessionID == "" || messageID == "" { + return ToolResponse{}, fmt.Errorf("session ID and message ID are required for patching a file") + } + + // Generate a diff for permission request and metadata + diffText, additions, removals := diff.GenerateDiff( + oldContent, + newContent, + params.FilePath, + ) + + // Request permission to apply the patch + p.permissions.Request( + permission.CreatePermissionRequest{ + Path: filepath.Dir(params.FilePath), + ToolName: PatchToolName, + Action: "patch", + Description: fmt.Sprintf("Apply patch to file %s", params.FilePath), + Params: PatchPermissionsParams{ + FilePath: params.FilePath, + Diff: diffText, + }, + }, + ) + + // Write the new content to the file + err = os.WriteFile(params.FilePath, []byte(newContent), 0o644) + if err != nil { + return ToolResponse{}, fmt.Errorf("failed to write file: %w", err) + } + + // Update file history + file, err := p.files.GetByPathAndSession(ctx, params.FilePath, sessionID) + if err != nil { + _, err = p.files.Create(ctx, sessionID, params.FilePath, oldContent) + if err != nil { + return ToolResponse{}, fmt.Errorf("error creating file history: %w", err) + } + } + if file.Content != oldContent { + // User manually changed the content, store an intermediate version + _, err = p.files.CreateVersion(ctx, sessionID, params.FilePath, oldContent) + if err != nil { + fmt.Printf("Error creating file history version: %v\n", err) + } + } + // Store the new version + _, err = p.files.CreateVersion(ctx, sessionID, params.FilePath, newContent) + if err != nil { + fmt.Printf("Error creating file history version: %v\n", err) + } + + recordFileWrite(params.FilePath) + recordFileRead(params.FilePath) + + // Wait for LSP diagnostics and include them in the response + waitForLspDiagnostics(ctx, params.FilePath, p.lspClients) + text := fmt.Sprintf("\nPatch applied to file: %s\n\n", params.FilePath) + text += getDiagnostics(params.FilePath, p.lspClients) + + return WithResponseMetadata( + NewTextResponse(text), + PatchResponseMetadata{ + Diff: diffText, + Additions: additions, + Removals: removals, + }), nil +} + +// applyPatch applies a parsed diff to a string and returns the resulting content +func applyPatch(content string, diffResult diff.DiffResult) (string, error) { + lines := strings.Split(content, "\n") + + // Process each hunk in the diff + for _, hunk := range diffResult.Hunks { + // Parse the hunk header to get line numbers + var oldStart, oldCount, newStart, newCount int + _, err := fmt.Sscanf(hunk.Header, "@@ -%d,%d +%d,%d @@", &oldStart, &oldCount, &newStart, &newCount) + if err != nil { + // Try alternative format with single line counts + _, err = fmt.Sscanf(hunk.Header, "@@ -%d +%d @@", &oldStart, &newStart) + if err != nil { + return "", fmt.Errorf("invalid hunk header format: %s", hunk.Header) + } + oldCount = 1 + newCount = 1 + } + + // Adjust for 0-based array indexing + oldStart-- + newStart-- + + // Apply the changes + newLines := make([]string, 0) + newLines = append(newLines, lines[:oldStart]...) + + // Process the hunk lines in order + currentOldLine := oldStart + for _, line := range hunk.Lines { + switch line.Kind { + case diff.LineContext: + newLines = append(newLines, line.Content) + currentOldLine++ + case diff.LineRemoved: + // Skip this line in the output (it's being removed) + currentOldLine++ + case diff.LineAdded: + // Add the new line + newLines = append(newLines, line.Content) + } + } + + // Append the rest of the file + newLines = append(newLines, lines[currentOldLine:]...) + lines = newLines + } + + return strings.Join(lines, "\n"), nil +} + diff --git a/internal/llm/tools/sourcegraph_test.go b/internal/llm/tools/sourcegraph_test.go deleted file mode 100644 index 89829aefc..000000000 --- a/internal/llm/tools/sourcegraph_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package tools - -import ( - "context" - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSourcegraphTool_Info(t *testing.T) { - tool := NewSourcegraphTool() - info := tool.Info() - - assert.Equal(t, SourcegraphToolName, info.Name) - assert.NotEmpty(t, info.Description) - assert.Contains(t, info.Parameters, "query") - assert.Contains(t, info.Parameters, "count") - assert.Contains(t, info.Parameters, "timeout") - assert.Contains(t, info.Required, "query") -} - -func TestSourcegraphTool_Run(t *testing.T) { - t.Run("handles missing query parameter", func(t *testing.T) { - tool := NewSourcegraphTool() - params := SourcegraphParams{ - Query: "", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: SourcegraphToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "Query parameter is required") - }) - - t.Run("handles invalid parameters", func(t *testing.T) { - tool := NewSourcegraphTool() - call := ToolCall{ - Name: SourcegraphToolName, - Input: "invalid json", - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "Failed to parse sourcegraph parameters") - }) - - t.Run("normalizes count parameter", func(t *testing.T) { - // Test cases for count normalization - testCases := []struct { - name string - inputCount int - expectedCount int - }{ - {"negative count", -5, 10}, // Should use default (10) - {"zero count", 0, 10}, // Should use default (10) - {"valid count", 50, 50}, // Should keep as is - {"excessive count", 150, 100}, // Should cap at 100 - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Verify count normalization logic directly - assert.NotPanics(t, func() { - // Apply the same normalization logic as in the tool - normalizedCount := tc.inputCount - if normalizedCount <= 0 { - normalizedCount = 10 - } else if normalizedCount > 100 { - normalizedCount = 100 - } - - assert.Equal(t, tc.expectedCount, normalizedCount) - }) - }) - } - }) -} diff --git a/internal/llm/tools/write_test.go b/internal/llm/tools/write_test.go deleted file mode 100644 index b5ecb3fda..000000000 --- a/internal/llm/tools/write_test.go +++ /dev/null @@ -1,307 +0,0 @@ -package tools - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "testing" - "time" - - "github.com/kujtimiihoxha/opencode/internal/lsp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestWriteTool_Info(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - info := tool.Info() - - assert.Equal(t, WriteToolName, info.Name) - assert.NotEmpty(t, info.Description) - assert.Contains(t, info.Parameters, "file_path") - assert.Contains(t, info.Parameters, "content") - assert.Contains(t, info.Required, "file_path") - assert.Contains(t, info.Required, "content") -} - -func TestWriteTool_Run(t *testing.T) { - // Create a temporary directory for testing - tempDir, err := os.MkdirTemp("", "write_tool_test") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - - t.Run("creates a new file successfully", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - filePath := filepath.Join(tempDir, "new_file.txt") - content := "This is a test content" - - params := WriteParams{ - FilePath: filePath, - Content: content, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "successfully written") - - // Verify file was created with correct content - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, content, string(fileContent)) - }) - - t.Run("creates file with nested directories", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt") - content := "Content in nested directory" - - params := WriteParams{ - FilePath: filePath, - Content: content, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "successfully written") - - // Verify file was created with correct content - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, content, string(fileContent)) - }) - - t.Run("updates existing file", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file first - filePath := filepath.Join(tempDir, "existing_file.txt") - initialContent := "Initial content" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record the file read to avoid modification time check failure - recordFileRead(filePath) - - // Update the file - updatedContent := "Updated content" - params := WriteParams{ - FilePath: filePath, - Content: updatedContent, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "successfully written") - - // Verify file was updated with correct content - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, updatedContent, string(fileContent)) - }) - - t.Run("handles invalid parameters", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - call := ToolCall{ - Name: WriteToolName, - Input: "invalid json", - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "error parsing parameters") - }) - - t.Run("handles missing file_path", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - params := WriteParams{ - FilePath: "", - Content: "Some content", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "file_path is required") - }) - - t.Run("handles missing content", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - params := WriteParams{ - FilePath: filepath.Join(tempDir, "file.txt"), - Content: "", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "content is required") - }) - - t.Run("handles writing to a directory path", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a directory - dirPath := filepath.Join(tempDir, "test_dir") - err := os.Mkdir(dirPath, 0o755) - require.NoError(t, err) - - params := WriteParams{ - FilePath: dirPath, - Content: "Some content", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "Path is a directory") - }) - - t.Run("handles permission denied", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(false), newMockFileHistoryService()) - - filePath := filepath.Join(tempDir, "permission_denied.txt") - params := WriteParams{ - FilePath: filePath, - Content: "Content that should not be written", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "Permission denied") - - // Verify file was not created - _, err = os.Stat(filePath) - assert.True(t, os.IsNotExist(err)) - }) - - t.Run("detects file modified since last read", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file - filePath := filepath.Join(tempDir, "modified_file.txt") - initialContent := "Initial content" - err := os.WriteFile(filePath, []byte(initialContent), 0o644) - require.NoError(t, err) - - // Record an old read time - fileRecordMutex.Lock() - fileRecords[filePath] = fileRecord{ - path: filePath, - readTime: time.Now().Add(-1 * time.Hour), - } - fileRecordMutex.Unlock() - - // Try to update the file - params := WriteParams{ - FilePath: filePath, - Content: "Updated content", - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "has been modified since it was last read") - - // Verify file was not modified - fileContent, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, initialContent, string(fileContent)) - }) - - t.Run("skips writing when content is identical", func(t *testing.T) { - tool := NewWriteTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService()) - - // Create a file - filePath := filepath.Join(tempDir, "identical_content.txt") - content := "Content that won't change" - err := os.WriteFile(filePath, []byte(content), 0o644) - require.NoError(t, err) - - // Record a read time - recordFileRead(filePath) - - // Try to write the same content - params := WriteParams{ - FilePath: filePath, - Content: content, - } - - paramsJSON, err := json.Marshal(params) - require.NoError(t, err) - - call := ToolCall{ - Name: WriteToolName, - Input: string(paramsJSON), - } - - response, err := tool.Run(context.Background(), call) - require.NoError(t, err) - assert.Contains(t, response.Content, "already contains the exact content") - }) -} diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index 54b39f4a1..fe2845a08 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -3,6 +3,7 @@ package chat import ( "context" "fmt" + "sort" "strings" tea "github.com/charmbracelet/bubbletea" @@ -141,8 +142,17 @@ func (m *sidebarCmp) modifiedFiles() string { ) } + // Sort file paths alphabetically for consistent ordering + var paths []string + for path := range m.modFiles { + paths = append(paths, path) + } + sort.Strings(paths) + + // Create views for each file in sorted order var fileViews []string - for path, stats := range m.modFiles { + for _, path := range paths { + stats := m.modFiles[path] fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals)) } diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index 200a7970d..295884432 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -266,6 +266,18 @@ func (p *permissionDialogCmp) renderEditContent() string { return "" } +func (p *permissionDialogCmp) renderPatchContent() string { + if pr, ok := p.permission.Params.(tools.PatchPermissionsParams); ok { + diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) { + return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width)) + }) + + p.contentViewPort.SetContent(diff) + return p.styleViewport() + } + return "" +} + func (p *permissionDialogCmp) renderWriteContent() string { if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok { // Use the cache for diff rendering @@ -350,6 +362,8 @@ func (p *permissionDialogCmp) render() string { contentFinal = p.renderBashContent() case tools.EditToolName: contentFinal = p.renderEditContent() + case tools.PatchToolName: + contentFinal = p.renderPatchContent() case tools.WriteToolName: contentFinal = p.renderWriteContent() case tools.FetchToolName: diff --git a/main.go b/main.go index 06578c7ef..2b0761c69 100644 --- a/main.go +++ b/main.go @@ -6,11 +6,9 @@ import ( ) func main() { - // Set up panic recovery for the main function defer logging.RecoverPanic("main", func() { - // Perform any necessary cleanup before exit logging.ErrorPersist("Application terminated due to unhandled panic") }) - + cmd.Execute() } -- cgit v1.2.3