diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 752014c53..3f8a4a91b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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"), }), ]), ) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 56d2545e7..7d93e50ac 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -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 diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 4a1ddca4e..4c6eaea1b 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -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", diff --git a/packages/opencode/src/lsp/severity.ts b/packages/opencode/src/lsp/severity.ts new file mode 100644 index 000000000..3671721c4 --- /dev/null +++ b/packages/opencode/src/lsp/severity.ts @@ -0,0 +1,13 @@ +export enum Severity { + ERROR = 1, + WARN = 2, + INFO = 3, + HINT = 4, +} + +export const SeverityMap: Record = { + ERROR: Severity.ERROR, + WARN: Severity.WARN, + INFO: Severity.INFO, + HINT: Severity.HINT, +} diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 8be41ecff..5ba9dee09 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -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\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n\n` continue } output += `\n\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\n` } diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 09dd20f42..818058fc9 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -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 diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index e83bd57ef..5d06106e8 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -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 diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index f7cb3c1a8..0e69fe27f 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -701,7 +701,7 @@ export type Config = { disabled: true } | { - command: Array + command?: Array extensions?: Array 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" } } /** diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index 15db37539..f51f849f4 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -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") }