Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2228,10 +2228,29 @@ function Skill(props: ToolProps<typeof SkillTool>) {

function Diagnostics(props: { diagnostics?: Record<string, Record<string, any>[]>; filePath: string }) {
const { theme } = useTheme()
const sync = useSync()
const errors = createMemo(() => {
const normalized = Filesystem.normalizePath(props.filePath)
const arr = props.diagnostics?.[normalized] ?? []
return arr.filter((x) => x.severity === 1).slice(0, 3)
const ext = path.extname(props.filePath)
let min = 1
// Look up min_severity from LSP config by matching file extension
const lspConfig = sync.data.config.lsp
if (lspConfig && typeof lspConfig === "object") {
for (const [id, cfg] of Object.entries(lspConfig)) {
if (!cfg || "disabled" in cfg) continue
const lsp = sync.data.lsp.find((l) => l.id === id)
if (lsp && "min_severity" in cfg && typeof cfg.min_severity === "number") {
// Check if this LSP handles this file extension
const exts = cfg.extensions
if (!exts || exts.includes(ext)) {
min = cfg.min_severity
break
}
}
}
}
return arr.filter((x) => (x.severity ?? 1) <= min).slice(0, 3)
})

return (
Expand Down
14 changes: 11 additions & 3 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1154,11 +1154,18 @@ 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(),
min_severity: z
.number()
.int()
.min(1)
.max(4)
.optional()
.describe("Minimum diagnostic severity to show (1=Error, 2=Warning, 3=Info, 4=Hint). Default: 1"),
}),
]),
),
Expand All @@ -1173,11 +1180,12 @@ export namespace Config {
return Object.entries(data).every(([id, config]) => {
if (config.disabled) return true
if (serverIds.has(id)) return true
return Boolean(config.extensions)
return Boolean(config.extensions) || Boolean(config.min_severity)
})
},
{
error: "For custom LSP servers, 'extensions' array is required.",
error:
"For custom LSP servers, 'extensions' array is required (unless only setting min_severity for a built-in server).",
},
),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
Expand Down
35 changes: 34 additions & 1 deletion packages/opencode/src/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,24 @@ export namespace LSP {
delete servers[name]
continue
}
// If no command provided, just update config for existing server
if (!item.command) {
if (existing) {
servers[name] = {
...existing,
extensions: item.extensions ?? existing.extensions,
}
}
continue
}
servers[name] = {
...existing,
id: name,
root: existing?.root ?? (async () => Instance.directory),
extensions: item.extensions ?? existing?.extensions ?? [],
spawn: async (root) => {
return {
process: spawn(item.command[0], item.command.slice(1), {
process: spawn(item.command![0], item.command!.slice(1), {
cwd: root,
windowsHide: true,
env: {
Expand Down Expand Up @@ -482,5 +492,28 @@ export namespace LSP {

return `${severity} [${line}:${col}] ${diagnostic.message}`
}

export function filter(items: LSPClient.Diagnostic[], min: number) {
return items.filter((d) => (d.severity ?? 1) <= min)
}
}

export async function minSeverity(file: string) {
const cfg = await Config.get()
if (!cfg.lsp || typeof cfg.lsp !== "object") return 1

const ext = path.extname(file)
const s = await state()

// Find servers that handle this file extension
for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(ext)) continue
const item = cfg.lsp[server.id]
if (item && !("disabled" in item && item.disabled) && "min_severity" in item && item.min_severity) {
return item.min_severity
}
}

return 1
}
}
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/apply_patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
const target = change.movePath ?? change.filePath
const normalized = Filesystem.normalizePath(target)
const issues = diagnostics[normalized] ?? []
const errors = issues.filter((item) => item.severity === 1)
const min = await LSP.minSeverity(target)
const errors = LSP.Diagnostic.filter(issues, min)
if (errors.length > 0) {
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
const suffix =
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ export const EditTool = Tool.define("edit", {
const diagnostics = await LSP.diagnostics()
const normalizedFilePath = Filesystem.normalizePath(filePath)
const issues = diagnostics[normalizedFilePath] ?? []
const errors = issues.filter((item) => item.severity === 1)
const min = await LSP.minSeverity(filePath)
const errors = LSP.Diagnostic.filter(issues, min)
if (errors.length > 0) {
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
const suffix =
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export const WriteTool = Tool.define("write", {
const normalizedFilepath = Filesystem.normalizePath(filepath)
let projectDiagnosticsCount = 0
for (const [file, issues] of Object.entries(diagnostics)) {
const errors = issues.filter((item) => item.severity === 1)
const min = await LSP.minSeverity(file)
const errors = LSP.Diagnostic.filter(issues, min)
if (errors.length === 0) continue
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
const suffix =
Expand Down
118 changes: 118 additions & 0 deletions packages/opencode/test/lsp/min-severity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect } from "bun:test"
import { Config } from "../../src/config/config"
import { LSP } from "../../src/lsp/index"

describe("LSP min_severity", () => {
describe("Config schema", () => {
it("accepts min_severity per-LSP", () => {
const testConfig = {
lsp: {
typescript: {
min_severity: 2
}
}
}

const result = Config.Info.safeParse(testConfig)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.lsp).toBeDefined()
expect((result.data.lsp as any).typescript.min_severity).toBe(2)
}
})

it("accepts min_severity with command and extensions", () => {
const testConfig = {
lsp: {
markdownlint: {
command: ["markdownlint-lsp"],
extensions: [".md"],
min_severity: 3
}
}
}

const result = Config.Info.safeParse(testConfig)
expect(result.success).toBe(true)
})

it("rejects min_severity > 4", () => {
const testConfig = {
lsp: {
typescript: {
min_severity: 5
}
}
}

const result = Config.Info.safeParse(testConfig)
expect(result.success).toBe(false)
})

it("rejects min_severity < 1", () => {
const testConfig = {
lsp: {
typescript: {
min_severity: 0
}
}
}

const result = Config.Info.safeParse(testConfig)
expect(result.success).toBe(false)
})

it("accepts non-integer min_severity as invalid", () => {
const testConfig = {
lsp: {
typescript: {
min_severity: 1.5
}
}
}

const result = Config.Info.safeParse(testConfig)
expect(result.success).toBe(false)
})
})

describe("LSP.Diagnostic.filter", () => {
const diagnostics = [
{ severity: 1, message: "Error", range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } },
{ severity: 2, message: "Warning", range: { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } } },
{ severity: 3, message: "Info", range: { start: { line: 2, character: 0 }, end: { line: 2, character: 0 } } },
{ severity: 4, message: "Hint", range: { start: { line: 3, character: 0 }, end: { line: 3, character: 0 } } },
] as any

it("returns only errors when min=1", () => {
const filtered = LSP.Diagnostic.filter(diagnostics, 1)
expect(filtered.length).toBe(1)
expect(filtered[0].message).toBe("Error")
})

it("returns errors and warnings when min=2", () => {
const filtered = LSP.Diagnostic.filter(diagnostics, 2)
expect(filtered.length).toBe(2)
expect(filtered.map(d => d.message)).toEqual(["Error", "Warning"])
})

it("returns errors, warnings, and info when min=3", () => {
const filtered = LSP.Diagnostic.filter(diagnostics, 3)
expect(filtered.length).toBe(3)
})

it("returns all diagnostics when min=4", () => {
const filtered = LSP.Diagnostic.filter(diagnostics, 4)
expect(filtered.length).toBe(4)
})

it("handles diagnostics without severity (defaults to 1)", () => {
const noSeverity = [
{ message: "No severity", range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } },
] as any

const filtered = LSP.Diagnostic.filter(noSeverity, 1)
expect(filtered.length).toBe(1)
})
})
})
Loading