mirror of
https://github.com/sst/opencode.git
synced 2025-08-22 05:54:08 +00:00
feat: lsp diagnostic severity configuration
This commit is contained in:
parent
50fb337270
commit
185b4e49da
9 changed files with 167 additions and 44 deletions
|
@ -14,6 +14,7 @@ import matter from "gray-matter"
|
|||
import { Flag } from "../flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
||||
import { LSP } from "../lsp"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
|
@ -382,11 +383,15 @@ export namespace Config {
|
|||
disabled: z.literal(true),
|
||||
}),
|
||||
z.object({
|
||||
command: z.array(z.string()),
|
||||
command: z.array(z.string()).optional(),
|
||||
extensions: z.array(z.string()).optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
initialization: z.record(z.string(), z.any()).optional(),
|
||||
severity: z
|
||||
.enum(Object.keys(LSP.Diagnostic.Severity) as [string, ...string[]])
|
||||
.optional()
|
||||
.describe("Minimum diagnostic severity level to show to agent"),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ import { LSPServer } from "./server"
|
|||
import { z } from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { spawn } from "child_process"
|
||||
import { SeverityMap } from "./severity"
|
||||
|
||||
export namespace LSP {
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
@ -73,18 +74,22 @@ export namespace LSP {
|
|||
...existing,
|
||||
root: existing?.root ?? (async (_file, app) => app.path.root),
|
||||
extensions: item.extensions ?? existing.extensions,
|
||||
spawn: async (_app, root) => {
|
||||
return {
|
||||
process: spawn(item.command[0], item.command.slice(1), {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
...item.env,
|
||||
},
|
||||
}),
|
||||
initialization: item.initialization,
|
||||
}
|
||||
},
|
||||
severity: item.severity ? SeverityMap[item.severity] : existing.severity,
|
||||
...(item.command && {
|
||||
spawn: async (_app, root) => {
|
||||
const command = item.command! // assertion since we checked above
|
||||
return {
|
||||
process: spawn(command[0], command.slice(1), {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
...item.env,
|
||||
},
|
||||
}),
|
||||
initialization: item.initialization,
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,15 +258,11 @@ export namespace LSP {
|
|||
}
|
||||
|
||||
export namespace Diagnostic {
|
||||
export function pretty(diagnostic: LSPClient.Diagnostic) {
|
||||
const severityMap = {
|
||||
1: "ERROR",
|
||||
2: "WARN",
|
||||
3: "INFO",
|
||||
4: "HINT",
|
||||
}
|
||||
export const Severity = SeverityMap
|
||||
export const Severities = Object.keys(Severity).filter((key) => isNaN(Number(key)))
|
||||
|
||||
const severity = severityMap[diagnostic.severity || 1]
|
||||
export function pretty(diagnostic: LSPClient.Diagnostic) {
|
||||
const severity = Severity[diagnostic.severity || 1]
|
||||
const line = diagnostic.range.start.line + 1
|
||||
const col = diagnostic.range.start.character + 1
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { BunProc } from "../bun"
|
|||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Severity } from "./severity"
|
||||
|
||||
export namespace LSPServer {
|
||||
const log = Log.create({ service: "lsp.server" })
|
||||
|
@ -34,6 +35,7 @@ export namespace LSPServer {
|
|||
|
||||
export interface Info {
|
||||
id: string
|
||||
severity?: Severity
|
||||
extensions: string[]
|
||||
global?: boolean
|
||||
root: RootFunction
|
||||
|
@ -128,6 +130,8 @@ export namespace LSPServer {
|
|||
|
||||
export const ESLint: Info = {
|
||||
id: "eslint",
|
||||
// most ESLint feedback has severity HINT
|
||||
severity: Severity.HINT,
|
||||
root: NearestRoot([
|
||||
"eslint.config.js",
|
||||
"eslint.config.mjs",
|
||||
|
|
13
packages/opencode/src/lsp/severity.ts
Normal file
13
packages/opencode/src/lsp/severity.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export enum Severity {
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
INFO = 3,
|
||||
HINT = 4,
|
||||
}
|
||||
|
||||
export const SeverityMap: Record<string, Severity> = {
|
||||
ERROR: Severity.ERROR,
|
||||
WARN: Severity.WARN,
|
||||
INFO: Severity.INFO,
|
||||
HINT: Severity.HINT,
|
||||
}
|
|
@ -16,6 +16,9 @@ import { Bus } from "../bus"
|
|||
import { FileTime } from "../file/time"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Config } from "../config/config"
|
||||
import { Severity, SeverityMap } from "../lsp/severity"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export const EditTool = Tool.define("edit", {
|
||||
description: DESCRIPTION,
|
||||
|
@ -34,6 +37,7 @@ export const EditTool = Tool.define("edit", {
|
|||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
const cfg = await Config.get()
|
||||
const app = App.info()
|
||||
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
if (!Filesystem.contains(app.path.cwd, filePath)) {
|
||||
|
@ -105,15 +109,28 @@ export const EditTool = Tool.define("edit", {
|
|||
let output = ""
|
||||
await LSP.touchFile(filePath, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
const lsp = cfg.lsp ?? {}
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
Log.Default.info("issue_s", {
|
||||
issues: issues.map((issue) => JSON.stringify(issue)),
|
||||
issueLength: issues.length,
|
||||
})
|
||||
if (issues.length === 0) continue
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues
|
||||
// TODO: may want to make more leniant for eslint
|
||||
.filter((item) => item.severity === 1)
|
||||
.filter((item) => {
|
||||
if (!item.source) {
|
||||
return item.severity === Severity.ERROR
|
||||
}
|
||||
const settings = lsp[item.source]
|
||||
if (settings.disabled) return false
|
||||
const minSeverity = settings.severity ? (SeverityMap[settings.severity] ?? Severity.ERROR) : Severity.ERROR
|
||||
|
||||
return item.severity && item.severity >= minSeverity
|
||||
})
|
||||
.map(LSP.Diagnostic.pretty)
|
||||
.join("\n")}\n</project_diagnostics>\n`
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
configured_endpoints: 39
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-e4b6496e5f2c68fa8b3ea1b88e40041eaf5ce2652001344df80bf130675d1766.yml
|
||||
openapi_spec_hash: df474311dc9e4a89cd483bd8b8d971d8
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-ae6510226d1ce153e1cc099a479b0b1cf0c2f5c905c6330b3fed9efac93f6086.yml
|
||||
openapi_spec_hash: 88261dd50416e611c62cbaf31b8a430d
|
||||
config_hash: eab3723c4c2232a6ba1821151259d6da
|
||||
|
|
|
@ -79,7 +79,8 @@ type Config struct {
|
|||
SmallModel string `json:"small_model"`
|
||||
Snapshot bool `json:"snapshot"`
|
||||
// Theme name to use for the interface
|
||||
Theme string `json:"theme"`
|
||||
Theme string `json:"theme"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
// TUI specific settings
|
||||
Tui ConfigTui `json:"tui"`
|
||||
// Custom username to display in conversations instead of system username
|
||||
|
@ -110,6 +111,7 @@ type configJSON struct {
|
|||
SmallModel apijson.Field
|
||||
Snapshot apijson.Field
|
||||
Theme apijson.Field
|
||||
Tools apijson.Field
|
||||
Tui apijson.Field
|
||||
Username apijson.Field
|
||||
raw string
|
||||
|
@ -803,9 +805,11 @@ type ConfigLsp struct {
|
|||
// This field can have the runtime type of [[]string].
|
||||
Extensions interface{} `json:"extensions"`
|
||||
// This field can have the runtime type of [map[string]interface{}].
|
||||
Initialization interface{} `json:"initialization"`
|
||||
JSON configLspJSON `json:"-"`
|
||||
union ConfigLspUnion
|
||||
Initialization interface{} `json:"initialization"`
|
||||
// Minimum diagnostic severity level to show to agent
|
||||
Severity ConfigLspSeverity `json:"severity"`
|
||||
JSON configLspJSON `json:"-"`
|
||||
union ConfigLspUnion
|
||||
}
|
||||
|
||||
// configLspJSON contains the JSON metadata for the struct [ConfigLsp]
|
||||
|
@ -815,6 +819,7 @@ type configLspJSON struct {
|
|||
Env apijson.Field
|
||||
Extensions apijson.Field
|
||||
Initialization apijson.Field
|
||||
Severity apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
@ -898,12 +903,14 @@ func (r ConfigLspDisabledDisabled) IsKnown() bool {
|
|||
}
|
||||
|
||||
type ConfigLspObject struct {
|
||||
Command []string `json:"command,required"`
|
||||
Command []string `json:"command"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Env map[string]string `json:"env"`
|
||||
Extensions []string `json:"extensions"`
|
||||
Initialization map[string]interface{} `json:"initialization"`
|
||||
JSON configLspObjectJSON `json:"-"`
|
||||
// Minimum diagnostic severity level to show to agent
|
||||
Severity ConfigLspObjectSeverity `json:"severity"`
|
||||
JSON configLspObjectJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configLspObjectJSON contains the JSON metadata for the struct [ConfigLspObject]
|
||||
|
@ -913,6 +920,7 @@ type configLspObjectJSON struct {
|
|||
Env apijson.Field
|
||||
Extensions apijson.Field
|
||||
Initialization apijson.Field
|
||||
Severity apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
@ -927,6 +935,42 @@ func (r configLspObjectJSON) RawJSON() string {
|
|||
|
||||
func (r ConfigLspObject) implementsConfigLsp() {}
|
||||
|
||||
// Minimum diagnostic severity level to show to agent
|
||||
type ConfigLspObjectSeverity string
|
||||
|
||||
const (
|
||||
ConfigLspObjectSeverityError ConfigLspObjectSeverity = "ERROR"
|
||||
ConfigLspObjectSeverityWarn ConfigLspObjectSeverity = "WARN"
|
||||
ConfigLspObjectSeverityInfo ConfigLspObjectSeverity = "INFO"
|
||||
ConfigLspObjectSeverityHint ConfigLspObjectSeverity = "HINT"
|
||||
)
|
||||
|
||||
func (r ConfigLspObjectSeverity) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigLspObjectSeverityError, ConfigLspObjectSeverityWarn, ConfigLspObjectSeverityInfo, ConfigLspObjectSeverityHint:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Minimum diagnostic severity level to show to agent
|
||||
type ConfigLspSeverity string
|
||||
|
||||
const (
|
||||
ConfigLspSeverityError ConfigLspSeverity = "ERROR"
|
||||
ConfigLspSeverityWarn ConfigLspSeverity = "WARN"
|
||||
ConfigLspSeverityInfo ConfigLspSeverity = "INFO"
|
||||
ConfigLspSeverityHint ConfigLspSeverity = "HINT"
|
||||
)
|
||||
|
||||
func (r ConfigLspSeverity) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigLspSeverityError, ConfigLspSeverityWarn, ConfigLspSeverityInfo, ConfigLspSeverityHint:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigMcp struct {
|
||||
// Type of MCP server connection
|
||||
Type ConfigMcpType `json:"type,required"`
|
||||
|
@ -1760,6 +1804,8 @@ type KeybindsConfig struct {
|
|||
SessionNew string `json:"session_new,required"`
|
||||
// Share current session
|
||||
SessionShare string `json:"session_share,required"`
|
||||
// Show session timeline
|
||||
SessionTimeline string `json:"session_timeline,required"`
|
||||
// Unshare current session
|
||||
SessionUnshare string `json:"session_unshare,required"`
|
||||
// @deprecated use agent_cycle. Next agent
|
||||
|
@ -1821,6 +1867,7 @@ type keybindsConfigJSON struct {
|
|||
SessionList apijson.Field
|
||||
SessionNew apijson.Field
|
||||
SessionShare apijson.Field
|
||||
SessionTimeline apijson.Field
|
||||
SessionUnshare apijson.Field
|
||||
SwitchAgent apijson.Field
|
||||
SwitchAgentReverse apijson.Field
|
||||
|
|
|
@ -701,7 +701,7 @@ export type Config = {
|
|||
disabled: true
|
||||
}
|
||||
| {
|
||||
command: Array<string>
|
||||
command?: Array<string>
|
||||
extensions?: Array<string>
|
||||
disabled?: boolean
|
||||
env?: {
|
||||
|
@ -710,6 +710,10 @@ export type Config = {
|
|||
initialization?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
/**
|
||||
* Minimum diagnostic severity level to show to agent
|
||||
*/
|
||||
severity?: "ERROR" | "WARN" | "INFO" | "HINT"
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
|
@ -564,7 +564,7 @@ func renderToolDetails(
|
|||
Padding(1, 2).
|
||||
Width(width - 4)
|
||||
|
||||
if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-6); diagnostics != "" {
|
||||
if diagnostics := renderDiagnostics(app.Config.Lsp, metadata, filename, backgroundColor, width-6); diagnostics != "" {
|
||||
diagnostics = style.Render(diagnostics)
|
||||
body += "\n" + diagnostics
|
||||
}
|
||||
|
@ -605,7 +605,7 @@ func renderToolDetails(
|
|||
if filename, ok := toolInputMap["filePath"].(string); ok {
|
||||
if content, ok := toolInputMap["content"].(string); ok {
|
||||
body = util.RenderFile(filename, content, width)
|
||||
if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-4); diagnostics != "" {
|
||||
if diagnostics := renderDiagnostics(app.Config.Lsp, metadata, filename, backgroundColor, width-4); diagnostics != "" {
|
||||
body += "\n\n" + diagnostics
|
||||
}
|
||||
}
|
||||
|
@ -925,12 +925,28 @@ type Diagnostic struct {
|
|||
Character int `json:"character"`
|
||||
} `json:"start"`
|
||||
} `json:"range"`
|
||||
Severity int `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
Severity int `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
Source *string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
var severityToLevel = map[int]string{
|
||||
1: "ERROR",
|
||||
2: "WARNING",
|
||||
3: "INFO",
|
||||
4: "HINT",
|
||||
}
|
||||
|
||||
var levelToSeverity = map[string]int{
|
||||
"ERROR": 1,
|
||||
"WARNING": 2,
|
||||
"INFO": 3,
|
||||
"HINT": 4,
|
||||
}
|
||||
|
||||
// renderDiagnostics formats LSP diagnostics for display in the TUI
|
||||
func renderDiagnostics(
|
||||
lspCfg map[string]opencode.ConfigLsp,
|
||||
metadata map[string]any,
|
||||
filePath string,
|
||||
backgroundColor compat.AdaptiveColor,
|
||||
|
@ -938,7 +954,7 @@ func renderDiagnostics(
|
|||
) string {
|
||||
if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
|
||||
if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
|
||||
var errorDiagnostics []string
|
||||
var diagnostics []string
|
||||
for _, diagInterface := range fileDiagnostics {
|
||||
diagMap, ok := diagInterface.(map[string]any)
|
||||
if !ok {
|
||||
|
@ -953,23 +969,39 @@ func renderDiagnostics(
|
|||
if err := json.Unmarshal(diagBytes, &diag); err != nil {
|
||||
continue
|
||||
}
|
||||
// Only show error diagnostics (severity === 1)
|
||||
if diag.Severity != 1 {
|
||||
|
||||
// if no LSP level configured then default to error level severity
|
||||
minSev := 1
|
||||
if diag.Source != nil {
|
||||
setting := lspCfg[*diag.Source]
|
||||
if setting.Severity.IsKnown() {
|
||||
minSev = levelToSeverity[string(setting.Severity)]
|
||||
}
|
||||
}
|
||||
if diag.Severity < minSev {
|
||||
continue
|
||||
}
|
||||
|
||||
level, ok := severityToLevel[diag.Severity]
|
||||
if !ok {
|
||||
// default to error
|
||||
level = "ERROR"
|
||||
}
|
||||
|
||||
line := diag.Range.Start.Line + 1 // 1-based
|
||||
column := diag.Range.Start.Character + 1 // 1-based
|
||||
errorDiagnostics = append(
|
||||
errorDiagnostics,
|
||||
fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message),
|
||||
|
||||
diagnostics = append(
|
||||
diagnostics,
|
||||
fmt.Sprintf("%s [%d:%d] %s", level, line, column, diag.Message),
|
||||
)
|
||||
}
|
||||
if len(errorDiagnostics) == 0 {
|
||||
if len(diagnostics) == 0 {
|
||||
return ""
|
||||
}
|
||||
t := theme.CurrentTheme()
|
||||
var result strings.Builder
|
||||
for _, diagnostic := range errorDiagnostics {
|
||||
for _, diagnostic := range diagnostics {
|
||||
if result.Len() > 0 {
|
||||
result.WriteString("\n\n")
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue