diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 764cc6d9c..a908a0314 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog ## [Unreleased] - ### Added +- Added SwiftLint linter client with JSON reporter support for Swift file linting - Added `--no-pty` flag to disable PTY-based interactive bash execution - Added `PI_NO_PTY` environment variable to disable PTY-based interactive bash execution - Added `bash.virtualTerminal` setting to control PTY-backed interactive execution for bash commands @@ -12,6 +12,8 @@ ### Changed +- Changed `bash.virtualTerminal` default from `on` to `off` for standard non-interactive bash execution +- Changed SwiftLint configuration to use `lint` command with JSON reporter instead of `analyze` for improved diagnostic parsing - Changed diff line format from space-separated (`+123 content`) to pipe-delimited (`+123|content`) for improved parsing reliability - Changed bash tool to use interactive PTY execution by default when UI is available, falling back to standard execution when disabled diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index f5b0e8b0a..e511e1dc0 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -633,7 +633,7 @@ export const SETTINGS_SCHEMA = { "bash.virtualTerminal": { type: "enum", values: ["on", "off"] as const, - default: "on", + default: "off", ui: { tab: "bash", label: "Virtual terminal", diff --git a/packages/coding-agent/src/lsp/clients/index.ts b/packages/coding-agent/src/lsp/clients/index.ts index 77cf16d64..5fa9b9325 100644 --- a/packages/coding-agent/src/lsp/clients/index.ts +++ b/packages/coding-agent/src/lsp/clients/index.ts @@ -10,6 +10,7 @@ import { LspLinterClient } from "./lsp-linter-client"; export { BiomeClient } from "./biome-client"; export { LspLinterClient } from "./lsp-linter-client"; +export { SwiftLintClient } from "./swiftlint-client"; // Cache of linter clients by server name + cwd const clientCache = new Map(); diff --git a/packages/coding-agent/src/lsp/clients/swiftlint-client.ts b/packages/coding-agent/src/lsp/clients/swiftlint-client.ts new file mode 100644 index 000000000..9a059de7d --- /dev/null +++ b/packages/coding-agent/src/lsp/clients/swiftlint-client.ts @@ -0,0 +1,120 @@ +/** + * SwiftLint CLI-based linter client. + * Parses SwiftLint's JSON reporter output into LSP Diagnostic format. + */ +import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types"; + +/** Shape of a single violation from `swiftlint lint --reporter json`. */ +interface SwiftLintViolation { + character: number; + file: string; + line: number; + reason: string; + rule_id: string; + severity: "Error" | "Warning"; + type: string; +} + +function parseSeverity(severity: string): DiagnosticSeverity { + switch (severity) { + case "Error": + return 1; + case "Warning": + return 2; + default: + return 2; + } +} + +async function runSwiftLint( + args: string[], + cwd: string, + resolvedCommand?: string, +): Promise<{ stdout: string; stderr: string; success: boolean }> { + const command = resolvedCommand ?? "swiftlint"; + + try { + const proc = Bun.spawn([command, ...args], { + cwd, + stdout: "pipe", + stderr: "pipe", + windowsHide: true, + }); + + const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]); + await proc.exited; + + // swiftlint exits non-zero when violations found — that's not a failure + return { stdout, stderr, success: stdout.length > 0 }; + } catch (err) { + return { stdout: "", stderr: String(err), success: false }; + } +} + +/** + * SwiftLint CLI-based linter client. + * Runs `swiftlint lint --reporter json` and converts violations to LSP diagnostics. + */ +export class SwiftLintClient implements LinterClient { + /** Factory method for creating SwiftLintClient instances */ + static create(config: ServerConfig, cwd: string): LinterClient { + return new SwiftLintClient(config, cwd); + } + + constructor( + private readonly config: ServerConfig, + private readonly cwd: string, + ) {} + + async format(_filePath: string, content: string): Promise { + // SwiftLint doesn't support formatting + return content; + } + + async lint(filePath: string): Promise { + const result = await runSwiftLint( + ["lint", "--quiet", "--reporter", "json", filePath], + this.cwd, + this.config.resolvedCommand, + ); + + if (!result.success) { + return []; + } + + return this.#parseJsonOutput(result.stdout); + } + + #parseJsonOutput(jsonOutput: string): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + + try { + const violations: SwiftLintViolation[] = JSON.parse(jsonOutput); + + for (const v of violations) { + // SwiftLint lines/characters are 1-based; LSP is 0-based + const line = Math.max(0, v.line - 1); + const character = Math.max(0, v.character - 1); + + diagnostics.push({ + range: { + start: { line, character }, + end: { line, character }, + }, + severity: parseSeverity(v.severity), + message: v.reason, + source: "swiftlint", + code: v.rule_id, + }); + } + } catch { + // JSON parse failed, return empty + } + + return diagnostics; + } + + dispose(): void { + // Nothing to dispose for CLI client + } +} diff --git a/packages/coding-agent/src/lsp/config.ts b/packages/coding-agent/src/lsp/config.ts index 4043e816e..7364d029b 100644 --- a/packages/coding-agent/src/lsp/config.ts +++ b/packages/coding-agent/src/lsp/config.ts @@ -6,6 +6,7 @@ import { YAML } from "bun"; import { globSync } from "glob"; import { getConfigDirPaths } from "../config"; import { BiomeClient } from "./clients/biome-client"; +import { SwiftLintClient } from "./clients/swiftlint-client"; import DEFAULTS from "./defaults.json" with { type: "json" }; import type { ServerConfig } from "./types"; @@ -137,6 +138,10 @@ function applyRuntimeDefaults(servers: Record): Record (arg === PID_TOKEN ? String(process.pid) : arg)); updated.omnisharp = { ...updated.omnisharp, args }; diff --git a/packages/coding-agent/src/lsp/defaults.json b/packages/coding-agent/src/lsp/defaults.json index 2e245e874..6009efba9 100644 --- a/packages/coding-agent/src/lsp/defaults.json +++ b/packages/coding-agent/src/lsp/defaults.json @@ -456,7 +456,7 @@ }, "swiftlint": { "command": "swiftlint", - "args": ["analyze", "--quiet"], + "args": ["lint", "--quiet", "--reporter", "json"], "fileTypes": [".swift"], "rootMarkers": [".swiftlint.yml", ".swiftlint.yaml", "Package.swift", "*.xcodeproj"], "isLinter": true diff --git a/packages/coding-agent/src/modes/components/settings-defs.ts b/packages/coding-agent/src/modes/components/settings-defs.ts index b0b2d1d83..9b8205573 100644 --- a/packages/coding-agent/src/modes/components/settings-defs.ts +++ b/packages/coding-agent/src/modes/components/settings-defs.ts @@ -139,6 +139,11 @@ const OPTION_PROVIDERS: Partial> = { { value: "20", label: "20 messages" }, { value: "30", label: "30 messages" }, ], + // Virtual terminal + "bash.virtualTerminal": [ + { value: "on", label: "On", description: "PTY-backed interactive execution" }, + { value: "off", label: "Off", description: "Standard non-interactive execution" }, + ], // Provider options "providers.webSearch": [ { value: "auto", label: "Auto", description: "Priority: Exa > Perplexity > Anthropic" },