Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit 2b5a33e

Browse files
committed
lsp improvements
1 parent bf8cd3b commit 2b5a33e

File tree

15 files changed

+921
-126
lines changed

15 files changed

+921
-126
lines changed

internal/app/app.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
4949
LSPClients: make(map[string]*lsp.Client),
5050
}
5151

52-
app.initLSPClients(ctx)
52+
// Initialize LSP clients in the background
53+
go app.initLSPClients(ctx)
5354

5455
var err error
5556
app.CoderAgent, err = agent.NewAgent(

internal/app/lsp.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,28 @@ func (app *App) initLSPClients(ctx context.Context) {
1515

1616
// Initialize LSP clients
1717
for name, clientConfig := range cfg.LSP {
18-
app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
18+
// Start each client initialization in its own goroutine
19+
go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
1920
}
21+
logging.Info("LSP clients initialization started in background")
2022
}
2123

2224
// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
2325
func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
2426
// Create a specific context for initialization with a timeout
25-
27+
logging.Info("Creating LSP client", "name", name, "command", command, "args", args)
28+
2629
// Create the LSP client
2730
lspClient, err := lsp.NewClient(ctx, command, args...)
2831
if err != nil {
2932
logging.Error("Failed to create LSP client for", name, err)
3033
return
31-
3234
}
3335

34-
initCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
36+
// Create a longer timeout for initialization (some servers take time to start)
37+
initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
3538
defer cancel()
39+
3640
// Initialize with the initialization context
3741
_, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
3842
if err != nil {
@@ -42,8 +46,25 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
4246
return
4347
}
4448

49+
// Wait for the server to be ready
50+
if err := lspClient.WaitForServerReady(initCtx); err != nil {
51+
logging.Error("Server failed to become ready", "name", name, "error", err)
52+
// We'll continue anyway, as some functionality might still work
53+
lspClient.SetServerState(lsp.StateError)
54+
} else {
55+
logging.Info("LSP server is ready", "name", name)
56+
lspClient.SetServerState(lsp.StateReady)
57+
}
58+
59+
logging.Info("LSP client initialized", "name", name)
60+
4561
// Create a child context that can be canceled when the app is shutting down
4662
watchCtx, cancelFunc := context.WithCancel(ctx)
63+
64+
// Create a context with the server name for better identification
65+
watchCtx = context.WithValue(watchCtx, "serverName", name)
66+
67+
// Create the workspace watcher
4768
workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
4869

4970
// Store the cancel function to be called during cleanup

internal/config/config.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,17 @@ func setProviderDefaults() {
209209
// Google Gemini configuration
210210
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
211211
viper.SetDefault("providers.gemini.apiKey", apiKey)
212-
viper.SetDefault("agents.coder.model", models.GRMINI20Flash)
213-
viper.SetDefault("agents.task.model", models.GRMINI20Flash)
214-
viper.SetDefault("agents.title.model", models.GRMINI20Flash)
212+
viper.SetDefault("agents.coder.model", models.Gemini25)
213+
viper.SetDefault("agents.task.model", models.Gemini25Flash)
214+
viper.SetDefault("agents.title.model", models.Gemini25Flash)
215215
}
216216

217217
// OpenAI configuration
218218
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
219219
viper.SetDefault("providers.openai.apiKey", apiKey)
220-
viper.SetDefault("agents.coder.model", models.GPT4o)
221-
viper.SetDefault("agents.task.model", models.GPT4o)
222-
viper.SetDefault("agents.title.model", models.GPT4o)
220+
viper.SetDefault("agents.coder.model", models.GPT41)
221+
viper.SetDefault("agents.task.model", models.GPT41Mini)
222+
viper.SetDefault("agents.title.model", models.GPT41Mini)
223223

224224
}
225225

internal/llm/models/gemini.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package models
2+
3+
const (
4+
ProviderGemini ModelProvider = "gemini"
5+
6+
// Models
7+
Gemini25Flash ModelID = "gemini-2.5-flash"
8+
Gemini25 ModelID = "gemini-2.5"
9+
Gemini20Flash ModelID = "gemini-2.0-flash"
10+
Gemini20FlashLite ModelID = "gemini-2.0-flash-lite"
11+
)
12+
13+
var GeminiModels = map[ModelID]Model{
14+
Gemini25Flash: {
15+
ID: Gemini25Flash,
16+
Name: "Gemini 2.5 Flash",
17+
Provider: ProviderGemini,
18+
APIModel: "gemini-2.5-flash-preview-04-17",
19+
CostPer1MIn: 0.15,
20+
CostPer1MInCached: 0,
21+
CostPer1MOutCached: 0,
22+
CostPer1MOut: 0.60,
23+
ContextWindow: 1000000,
24+
DefaultMaxTokens: 50000,
25+
},
26+
Gemini25: {
27+
ID: Gemini25,
28+
Name: "Gemini 2.5 Pro",
29+
Provider: ProviderGemini,
30+
APIModel: "gemini-2.5-pro-preview-03-25",
31+
CostPer1MIn: 1.25,
32+
CostPer1MInCached: 0,
33+
CostPer1MOutCached: 0,
34+
CostPer1MOut: 10,
35+
ContextWindow: 1000000,
36+
DefaultMaxTokens: 50000,
37+
},
38+
39+
Gemini20Flash: {
40+
ID: Gemini20Flash,
41+
Name: "Gemini 2.0 Flash",
42+
Provider: ProviderGemini,
43+
APIModel: "gemini-2.0-flash",
44+
CostPer1MIn: 0.10,
45+
CostPer1MInCached: 0,
46+
CostPer1MOutCached: 0,
47+
CostPer1MOut: 0.40,
48+
ContextWindow: 1000000,
49+
DefaultMaxTokens: 6000,
50+
},
51+
Gemini20FlashLite: {
52+
ID: Gemini20FlashLite,
53+
Name: "Gemini 2.0 Flash Lite",
54+
Provider: ProviderGemini,
55+
APIModel: "gemini-2.0-flash-lite",
56+
CostPer1MIn: 0.05,
57+
CostPer1MInCached: 0,
58+
CostPer1MOutCached: 0,
59+
CostPer1MOut: 0.30,
60+
ContextWindow: 1000000,
61+
DefaultMaxTokens: 6000,
62+
},
63+
}

internal/llm/models/models.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ type Model struct {
2323

2424
// Model IDs
2525
const ( // GEMINI
26-
GEMINI25 ModelID = "gemini-2.5"
27-
GRMINI20Flash ModelID = "gemini-2.0-flash"
28-
2926
// GROQ
3027
QWENQwq ModelID = "qwen-qwq"
3128

@@ -35,7 +32,6 @@ const ( // GEMINI
3532

3633
const (
3734
ProviderBedrock ModelProvider = "bedrock"
38-
ProviderGemini ModelProvider = "gemini"
3935
ProviderGROQ ModelProvider = "groq"
4036

4137
// ForTests
@@ -95,4 +91,5 @@ var SupportedModels = map[ModelID]Model{
9591
func init() {
9692
maps.Copy(SupportedModels, AnthropicModels)
9793
maps.Copy(SupportedModels, OpenAIModels)
94+
maps.Copy(SupportedModels, GeminiModels)
9895
}

internal/llm/prompt/coder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ You MUST adhere to the following criteria when executing the task:
6868
- Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them.
6969
- When doing things with paths, always use use the full path, if the working directory is /abc/xyz and you want to edit the file abc.go in the working dir refer to it as /abc/xyz/abc.go.
7070
- If you send a path not including the working dir, the working dir will be prepended to it.
71+
- Remember the user does not see the full output of tools
7172
`
7273

7374
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.
@@ -162,6 +163,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
162163
# Tool usage policy
163164
- When doing file search, prefer to use the Agent tool in order to reduce context usage.
164165
- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block.
166+
- IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
165167
166168
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.`
167169

internal/llm/provider/gemini.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,4 +567,3 @@ func contains(s string, substrs ...string) bool {
567567
}
568568
return false
569569
}
570-

internal/llm/tools/grep.go

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111
"regexp"
1212
"sort"
13+
"strconv"
1314
"strings"
1415
"time"
1516

@@ -24,8 +25,10 @@ type GrepParams struct {
2425
}
2526

2627
type grepMatch struct {
27-
path string
28-
modTime time.Time
28+
path string
29+
modTime time.Time
30+
lineNum int
31+
lineText string
2932
}
3033

3134
type GrepResponseMetadata struct {
@@ -147,13 +150,26 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
147150
if len(matches) == 0 {
148151
output = "No files found"
149152
} else {
150-
output = fmt.Sprintf("Found %d file%s\n%s",
151-
len(matches),
152-
pluralize(len(matches)),
153-
strings.Join(matches, "\n"))
153+
output = fmt.Sprintf("Found %d matches\n", len(matches))
154+
155+
currentFile := ""
156+
for _, match := range matches {
157+
if currentFile != match.path {
158+
if currentFile != "" {
159+
output += "\n"
160+
}
161+
currentFile = match.path
162+
output += fmt.Sprintf("%s:\n", match.path)
163+
}
164+
if match.lineNum > 0 {
165+
output += fmt.Sprintf(" Line %d: %s\n", match.lineNum, match.lineText)
166+
} else {
167+
output += fmt.Sprintf(" %s\n", match.path)
168+
}
169+
}
154170

155171
if truncated {
156-
output += "\n\n(Results are truncated. Consider using a more specific path or pattern.)"
172+
output += "\n(Results are truncated. Consider using a more specific path or pattern.)"
157173
}
158174
}
159175

@@ -166,14 +182,7 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
166182
), nil
167183
}
168184

169-
func pluralize(count int) string {
170-
if count == 1 {
171-
return ""
172-
}
173-
return "s"
174-
}
175-
176-
func searchFiles(pattern, rootPath, include string, limit int) ([]string, bool, error) {
185+
func searchFiles(pattern, rootPath, include string, limit int) ([]grepMatch, bool, error) {
177186
matches, err := searchWithRipgrep(pattern, rootPath, include)
178187
if err != nil {
179188
matches, err = searchFilesWithRegex(pattern, rootPath, include)
@@ -191,12 +200,7 @@ func searchFiles(pattern, rootPath, include string, limit int) ([]string, bool,
191200
matches = matches[:limit]
192201
}
193202

194-
results := make([]string, len(matches))
195-
for i, m := range matches {
196-
results[i] = m.path
197-
}
198-
199-
return results, truncated, nil
203+
return matches, truncated, nil
200204
}
201205

202206
func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
@@ -205,7 +209,8 @@ func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
205209
return nil, fmt.Errorf("ripgrep not found: %w", err)
206210
}
207211

208-
args := []string{"-l", pattern}
212+
// Use -n to show line numbers and include the matched line
213+
args := []string{"-n", pattern}
209214
if include != "" {
210215
args = append(args, "--glob", include)
211216
}
@@ -228,14 +233,29 @@ func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
228233
continue
229234
}
230235

231-
fileInfo, err := os.Stat(line)
236+
// Parse ripgrep output format: file:line:content
237+
parts := strings.SplitN(line, ":", 3)
238+
if len(parts) < 3 {
239+
continue
240+
}
241+
242+
filePath := parts[0]
243+
lineNum, err := strconv.Atoi(parts[1])
244+
if err != nil {
245+
continue
246+
}
247+
lineText := parts[2]
248+
249+
fileInfo, err := os.Stat(filePath)
232250
if err != nil {
233251
continue // Skip files we can't access
234252
}
235253

236254
matches = append(matches, grepMatch{
237-
path: line,
238-
modTime: fileInfo.ModTime(),
255+
path: filePath,
256+
modTime: fileInfo.ModTime(),
257+
lineNum: lineNum,
258+
lineText: lineText,
239259
})
240260
}
241261

@@ -276,15 +296,17 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
276296
return nil
277297
}
278298

279-
match, err := fileContainsPattern(path, regex)
299+
match, lineNum, lineText, err := fileContainsPattern(path, regex)
280300
if err != nil {
281301
return nil // Skip files we can't read
282302
}
283303

284304
if match {
285305
matches = append(matches, grepMatch{
286-
path: path,
287-
modTime: info.ModTime(),
306+
path: path,
307+
modTime: info.ModTime(),
308+
lineNum: lineNum,
309+
lineText: lineText,
288310
})
289311

290312
if len(matches) >= 200 {
@@ -301,21 +323,24 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
301323
return matches, nil
302324
}
303325

304-
func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, error) {
326+
func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, string, error) {
305327
file, err := os.Open(filePath)
306328
if err != nil {
307-
return false, err
329+
return false, 0, "", err
308330
}
309331
defer file.Close()
310332

311333
scanner := bufio.NewScanner(file)
334+
lineNum := 0
312335
for scanner.Scan() {
313-
if pattern.MatchString(scanner.Text()) {
314-
return true, nil
336+
lineNum++
337+
line := scanner.Text()
338+
if pattern.MatchString(line) {
339+
return true, lineNum, line, nil
315340
}
316341
}
317342

318-
return false, scanner.Err()
343+
return false, 0, "", scanner.Err()
319344
}
320345

321346
func globToRegex(glob string) string {

0 commit comments

Comments
 (0)