42ffc83b5d
- Re-polled steering at the loop yield boundary and included it in the pre-stop pending batch so late messages are processed immediately. - Added session-side draining for stranded queued messages, scheduling an auto-continue when a prompt settles and follow-ups or steers remain. - Added a regression test for late steering injection at yield and updated mid-turn collab prompt handling to keep steering messages in the pending display queue until consumed.
23 KiB
23 KiB
lsp
Query language servers for diagnostics, navigation, symbols, renames, code actions, capabilities, and raw requests.
Source
- Entry:
packages/coding-agent/src/lsp/index.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/lsp.md - Key collaborators:
packages/coding-agent/src/lsp/client.ts— client process lifecycle and JSON-RPCpackages/coding-agent/src/lsp/config.ts— config loading, auto-detect, server selectionpackages/coding-agent/src/lsp/lspmux.ts— optionallspmuxcommand wrappingpackages/coding-agent/src/lsp/edits.ts— applyWorkspaceEditand text editspackages/coding-agent/src/lsp/utils.ts— URI conversion, symbol resolution, formatting, glob expansionpackages/coding-agent/src/lsp/types.ts— tool schema and protocol typespackages/coding-agent/src/lsp/clients/index.ts— custom linter client cache/factorypackages/coding-agent/src/lsp/clients/lsp-linter-client.ts— LSP-backed linter adapterpackages/coding-agent/src/lsp/clients/biome-client.ts— Biome CLI diagnostics/formatting adapterpackages/coding-agent/src/lsp/clients/swiftlint-client.ts— SwiftLint CLI diagnostics adapterpackages/coding-agent/src/tools/index.ts— tool registration andlsp.enabledgatingpackages/coding-agent/src/tools/tool-timeouts.ts— timeout defaults and clampingpackages/coding-agent/src/lsp/defaults.json— built-in server definitions for auto-detect
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
action |
string enum | Yes | One of diagnostics, definition, references, hover, symbols, rename, rename_file, code_actions, type_definition, implementation, status, reload, capabilities, request. |
file |
string | No | File path; for diagnostics also a glob; for workspace forms use "*"; for rename_file this is the source path. |
line |
number | No | 1-indexed line number for position-based actions. Defaults to 1 on the single-file action path. |
symbol |
string | No | Substring used to resolve the column on line. Supports name#N occurrence selectors; N is 1-indexed and defaults to 1. Required when line is given for definition/references/rename against project-aware servers. |
query |
string | No | Workspace symbol query, code-action selector/filter, or LSP method name for action=request. |
new_name |
string | No | Required for rename and rename_file. |
apply |
boolean | No | For rename/rename_file, apply unless explicitly false. For code_actions, list unless explicitly true. |
timeout |
number | No | Seconds, clamped by clampTimeout("lsp", ...) to 5..60, default 20. |
payload |
string | No | JSON string for action=request; overrides auto-built params. |
Outputs
- Single-shot
AgentToolResult. contentis always one text block:[{ type: "text", text: string }].detailsisLspToolDetails:action,success, optionalserverName, optional originalrequest.- No streaming updates.
- No artifact URIs or background jobs.
- Many validation failures are returned as ordinary text results with
details.success: false; aborts throwToolAbortErrorinstead.
Flow
packages/coding-agent/src/tools/index.tsregisterslsp: LspTool.createIf; session creation also gates it behindsession.enableLsp !== falseandsettings.get("lsp.enabled").LspTool.execute()inpackages/coding-agent/src/lsp/index.tsclampstimeoutwithclampTimeout("lsp", ...), builds anAbortSignal.timeout(...), and combines it with the caller signal.getConfig()loads and cachesLspConfigper cwd, applies idle-timeout config viasetIdleTimeout(), and reuses the cached config on later calls.- Config loading in
packages/coding-agent/src/lsp/config.tsmergesdefaults.jsonwith JSON/YAML overrides from project, project config dirs, user config dirs, plugin roots, and home; if there are no overrides it auto-detects servers from root markers plus executable discovery. - Server routing uses
getServersForFile()/getServerForFile()fromconfig.ts: extension or basename match, then sort primary servers before linters.index.tsfurther filters custom linter clients out of navigation/refactor paths withgetLspServersForFile()/getLspServerForFile(). getOrCreateClient()inclient.tscreates one process percommand:cwd, optionally wraps supported commands withlspmux, spawns the server, starts the background message reader, sendsinitialize, stores server capabilities, then sendsinitialized.- The message reader in
client.tsparses LSP frames, resolves pending requests, cachespublishDiagnostics, tracks$/progresstokens for project-load completion, answersworkspace/configuration, and appliesworkspace/applyEditrequests throughapplyWorkspaceEdit(). - File-scoped actions call
ensureFileOpen()before requests. Column resolution usesresolveSymbolColumn()fromutils.ts: read the target file, pick first non-whitespace whensymbolis omitted, otherwise find the exact or case-insensitive match on the target line and honor#Noccurrence selectors. - Actions dispatch in
LspTool.execute()through dedicated branches: workspace-only branches (status, somediagnostics, workspacesymbols, workspacereload,capabilities,request) run before the single-file switch; all other single-file actions share one client lookup andswitch(action). - Requests go through
sendRequest()inclient.ts, which allocates an incrementing JSON-RPC id, installs abort and timeout handling, sends$/cancelRequeston abort, and rejects on timeout or process exit. - Actions that return edits either preview with
formatWorkspaceEdit()or apply withapplyWorkspaceEdit()fromedits.ts;rename_filealso performs the filesystem rename and then sendsworkspace/didRenameFiles. - Non-abort failures inside the single-file action block are converted to
LSP error: ...; many precondition failures return explicit text without throwing.
Modes / Variants
Routing and workspace scope
file: "*"is only special fordiagnostics,symbols, andreload.statusignoresfile.capabilitieswith omittedfileor"*"inspects all non-custom LSP servers; with a concrete file it scopes to matching non-custom servers.requestwith omittedfileor"*"chooses the first available non-custom LSP server; with a concrete file it chooses that file's primary non-linter server.rename_filesendsworkspace/willRenameFilesandworkspace/didRenameFilesto every non-custom LSP server fromgetLspServers(config)whosefileTypesmatch the source, destination, or any enumerated rename pair — not just one file-scoped server.- Diagnostics are the only tool action that queries both normal LSP servers and custom linter clients (
BiomeClient,SwiftLintClient, orLspLinterClient).
diagnostics
Inputs
- Required:
file, unless using workspace mode withfile: "*". - Optional:
timeout.
Execution
file: "*":runWorkspaceDiagnostics()detects project type from root markers and runs one subprocess command: Rustcargo check --message-format=short, TypeScriptnpx tsc --noEmit, Pythonpyright.- Concrete file or glob:
resolveDiagnosticTargets()treats non-globs as one target, otherwise expands aBun.Globup toMAX_GLOB_DIAGNOSTIC_TARGETS. - Per file, every matching server runs: custom clients call
lint(file); real LSP servers optionally wait for project load, capturediagnosticsVersion,refreshFile(), thenwaitForDiagnostics()for freshpublishDiagnostics(settles on the latest publish; exact-version match accepted immediately). - Results are deduplicated by range+message and severity-sorted.
Output text
- Single target with no issues:
OK. - Single target with issues:
<summary>:\n<grouped diagnostics>. - Batch/glob target: one section per file, plus an initial truncation warning when the glob exceeds the file cap.
- Workspace mode:
Workspace diagnostics (<detected description>):\n<command output>.
definition
Inputs
- Required:
file. - Optional:
line,symbol,timeout.
Execution
- Sends
textDocument/definitionwith{ textDocument, position }. - Accepts
Location,Location[],LocationLink, orLocationLink[];normalizeLocationResult()convertsLocationLinktotargetSelectionRange ?? targetRange. - Requires
symbolwhenlineis given on project-aware servers (the first-non-whitespace-column fallback is disabled for this action). - Waits for project load before the request.
Output text
No definition foundorFound N definition(s):followed byfile:line:coland one context line above/below each location.
type_definition
Same as definition, but sends textDocument/typeDefinition and reports type definition(s).
implementation
Same as definition, but sends textDocument/implementation and reports implementation(s).
references
Inputs
- Required:
file. - Optional:
line,symbol,timeout.
Execution
- Sends
textDocument/referenceswithincludeDeclaration: true. - Requires
symbolwhenlineis given on project-aware servers (the first-non-whitespace-column fallback is disabled for this action). - For project-aware servers, retries up to
REFERENCES_RETRY_COUNTtimes when the only hit is the queried declaration; between retries it waits for project load and sleepsREFERENCES_RETRY_DELAY_MS. - First
REFERENCE_CONTEXT_LIMITreferences include surrounding context; the rest are location-only.
Output text
No references foundorFound N reference(s):with contextual entries first, then... M additional reference(s) shown without contextwhen truncated.
hover
Inputs
- Required:
file. - Optional:
line,symbol,timeout.
Execution
- Sends
textDocument/hover. extractHoverText()flattens strings, markup content, marked-string objects, or arrays into plain text.
Output text
No hover informationor the extracted hover text.
symbols
Inputs
- Workspace mode:
file: "*"or omitted file on the early workspace branch, plus requiredquery. - Document mode: required
file. - Optional:
timeout.
Execution
- Workspace mode sends
workspace/symbolto every non-custom LSP server, post-filters matches withfilterWorkspaceSymbols(), deduplicates withdedupeWorkspaceSymbols(), then truncates toWORKSPACE_SYMBOL_LIMIT. - Document mode sends
textDocument/documentSymbolto the primary server. If the first item hasselectionRange, it formats hierarchicalDocumentSymbols; otherwise it formats flatSymbolInformations.
Output text
- Workspace mode:
Found N symbol(s) matching "query":plus formattedname @ file:line:col, with an omission line when over the limit. - Document mode:
Symbols in <file>:plus hierarchical or flat symbol lines.
rename
Inputs
- Required:
file,new_name. - Optional:
line,symbol,apply,timeout.
Execution
- Requires
symbolwhenlineis given on project-aware servers, then waits for project load, sendstextDocument/rename, receives aWorkspaceEdit. apply !== falseapplies edits immediately withapplyWorkspaceEdit().apply === falserenders a preview withformatWorkspaceEdit().
Output text
Rename returned no edits,Applied rename:plus applied change lines, orRename preview:plus summarized edits.
rename_file
Inputs
- Required:
filesource path,new_namedestination path. - Optional:
apply,timeout.
Execution
- Resolves absolute source and destination, rejects identical paths, missing source, existing destination, empty rename set, or directories with more than
MAX_RENAME_PAIRSfiles. enumerateRenamePairs()returns one{oldUri,newUri}pair for a file or walks every regular file in a directory tree.- Sends
workspace/willRenameFileswith{ files: pairs }to every non-custom LSP server whosefileTypesmatch an affected path; collects returnedWorkspaceEdits and server notes. - Preview mode (
apply === false) only formats those edits. - Apply mode coalesces the returned text edits per URI (a project-aware server's edits win on overlap; overlapping edits from other servers are discarded with a note), applies each URI once from a single snapshot, creates the destination parent directory and renames the source path on disk, sends
textDocument/didClosefor every renamed open file, deletes thoseopenFilesentries, then sendsworkspace/didRenameFiles.
Output text
- Preview:
Rename preview: <file-count label> → <dest>plus per-server edit summaries and optional server notes. - Apply:
Renamed <file-count label> → <dest>plus applied edit summaries, filesystem rename line, and optional server notes.
code_actions
Inputs
- Required:
file. - Optional:
line,symbol,query,apply,timeout.
Execution
- Reads cached diagnostics for the open URI from
client.diagnosticsand sendstextDocument/codeActionfor a zero-width range at the resolved position. - When
apply !== true,queryis passed ascontext.only: [query]; this is a server-side kind filter. - When
apply === true,querybecomes a required client-side selector: either a zero-based numeric index or a case-insensitive substring of the action title. - Applying a
CodeActionusesapplyCodeAction(): optionallycodeAction/resolve, thenapplyWorkspaceEdit(edit), then optionalworkspace/executeCommand. - Applying a bare
Commandonly runsworkspace/executeCommand.
Output text
- List mode:
N code action(s):plusindex: [kind] titlelines. - Apply mode success:
Applied "title":plusWorkspace edit:and/orExecuted command(s):sections. - Apply mode miss:
No code action matches "query". Available actions:. - Apply mode with no edit/command:
Action "title" has no workspace edit or command to apply.
status
Inputs
- None.
Execution
- Reads configured servers from cached
LspConfigand cross-referencesgetActiveClients()so each server is labelled(configured, not started)or with its live client status. - Calls
detectLspmux()and appends status text whenlspmuxis installed.
Output text
Language servers: <name (configured, not started) | name (<status>)>plus an explanatory note line, orNo language servers configured for this project, optionally followed bylspmux: active (multiplexing enabled)orlspmux: installed but server not running.
reload
Inputs
- Workspace mode:
file: "*"or omittedfile. - Single-file mode: required
file. - Optional:
timeout.
Execution
- Workspace mode reloads every non-custom LSP server.
- Single-file mode reloads the primary server for that file.
reloadServer()triesrust-analyzer/reloadWorkspace, thenworkspace/didChangeConfigurationwith{ settings: {} }; if neither works it kills the process so the next request cold-starts a new client.
Output text
- One line per server:
Reloaded <server>,Restarted <server>, orFailed to reload <server>: ....
capabilities
Inputs
- Optional:
file,timeout.
Execution
- With a concrete
file, inspects matching non-custom servers for that file. - With omitted
fileor"*", inspects every non-custom configured server. - Starts servers as needed and dumps
client.serverCapabilities ?? {}as pretty JSON.
Output text
- Per server:
<server>:followed by indentedcapabilities: { ... }, or<server>: failed to start (...).
request
Inputs
- Required:
querymethod name. - Optional:
file,line,symbol,payload,timeout.
Execution
- Chooses one non-custom server: file-scoped primary server, otherwise the first configured non-custom server.
- Param building precedence:
- If
payloadis present, parse JSON and use it verbatim. - Else if
fileis concrete andlineis present, build{ textDocument: { uri }, position: { line: line - 1, character } }usingresolveSymbolColumn(). - Else if
fileis concrete, build{ textDocument: { uri } }. - Else use
{}.
- If
- Opens the file first when
fileis concrete.
Output text
- Success:
<server> ← <method>:\n<formatted result>, where non-string results areJSON.stringify(..., null, 2)and nullish values becomenull. - Failure:
LSP error from <server> on <method>: ...followed byparams: <preview>echoing the request params (truncated to 400 chars).
Side Effects
- Filesystem
- Reads config files, target files, and root markers.
renameandcode_actionsmay edit/create/delete/rename files viaapplyWorkspaceEdit().rename_filealways renames the source path on disk in apply mode.- Server-initiated
workspace/applyEditrequests also mutate files throughapplyWorkspaceEdit().
- Network
- None directly; communication is local stdio JSON-RPC to subprocesses.
- Subprocesses / native bindings
- Spawns language servers with
ptree.spawn(). - Workspace diagnostics spawns
cargo,npx,go, orpyright. BiomeClientandSwiftLintClientspawn CLI tools.- Optional
lspmuxdetection spawnslspmux status; supported servers may be wrapped throughlspmux client.
- Spawns language servers with
- Session state (transcript, memory, jobs, checkpoints, registries)
- Caches config per cwd in
configCache. - Caches LSP clients per
command:cwd, withpendingRequests,diagnostics,openFiles,serverCapabilities, and project-load state. - Caches custom linter clients by
serverName:cwd. - Updates client
lastActivity; optional idle-timeout cleanup is driven bysetIdleTimeout().
- Caches config per cwd in
- Background work / cancellation
- Every request has an abortable timeout signal.
- Aborting an in-flight LSP request sends
$/cancelRequest. - Background message readers persist for each live client until process exit/shutdown.
Limits & Caps
- Tool timeout clamp: default
20, min5, max60seconds —TOOL_TIMEOUTS.lspinpackages/coding-agent/src/tools/tool-timeouts.ts. - LSP request default timeout inside
sendRequest():30_000ms—DEFAULT_REQUEST_TIMEOUT_MSinpackages/coding-agent/src/lsp/client.ts. - Warmup initialize timeout default:
5_000ms—WARMUP_TIMEOUT_MSinpackages/coding-agent/src/lsp/client.ts. - Project-load wait fallback:
15_000ms—PROJECT_LOAD_TIMEOUT_MSinpackages/coding-agent/src/lsp/client.ts. - Idle-client sweep interval when enabled:
60_000ms—IDLE_CHECK_INTERVAL_MSinpackages/coding-agent/src/lsp/client.ts. - Diagnostic message output cap: first
50messages —DIAGNOSTIC_MESSAGE_LIMITinpackages/coding-agent/src/lsp/index.ts. - Single-file diagnostics wait:
3_000ms—SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS. - Batch/glob diagnostics wait per file:
400ms—BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS. - Glob diagnostic target cap: first
20matches —MAX_GLOB_DIAGNOSTIC_TARGETS. - Workspace symbol cap: first
200entries —WORKSPACE_SYMBOL_LIMIT. - Reference context cap: first
50references include source context —REFERENCE_CONTEXT_LIMIT. - References retry count:
2retries,250msbackoff —REFERENCES_RETRY_COUNT,REFERENCES_RETRY_DELAY_MS. - Directory rename cap:
1_000file pairs —MAX_RENAME_PAIRS. detectLspmux()state cache TTL:5 * 60 * 1000ms; liveness check timeout:1_000ms—STATE_CACHE_TTL_MS,LIVENESS_TIMEOUT_MSinpackages/coding-agent/src/lsp/lspmux.ts.- Workspace diagnostics output cap: first
50lines from the subprocess.
Errors
- Missing or invalid inputs are usually returned as text with
details.success: false, not thrown:- missing
file/query/new_name - invalid JSON in
payload - no matching server
- invalid
rename_filesource/destination conditions
- missing
resolveSymbolColumn()throws explicit errors for missing files, missing symbols, and out-of-bounds#Nselectors; these surface asLSP error: ...or request-specific error text.sendRequest()rejects on timeout withLSP request <method> timed out after <ms>ms.- Client process exit rejects all pending requests with an exit-code/stderr error assembled in
getOrCreateClient(). - Single-file action failures inside the main
trybecomeLSP error: <message>. requesthas its own error envelope:LSP error from <server> on <method>: <message>.- Some server failures are intentionally softened:
- diagnostics continue when one server fails
rename_filesuppressesworkspace/willRenameFiles“method not found” errors and records other server errors as notescode_actionsignorescodeAction/resolvefailures and applies unresolved actions when possible
- Caller aborts are not converted to text:
ToolAbortErroris rethrown. A wall-clock tool timeout without a caller abort instead throwsToolError:LSP <action> timed out after <N>s on <server>. ....
Notes
statusreports configured servers fromLspConfigand labels each one viagetActiveClients():(configured, not started)means the binary resolves on PATH but no request has spawned it; a live client reports its status.getLspServerForFile()excludescreateClientadapters and linter-only servers; navigation/refactor actions never target Biome/SwiftLint custom clients.getServersForFile()matches both file extensions and exact basenames fromfileTypes; config can target names likeDockerfileif present.symbolmatching is exact first, then case-insensitive, and falls back to the Nth occurrence on the specified line only; it never scans other lines.- For
definition,references, andrenameagainst project-aware servers, omittingsymbolwhile passinglineis rejected with aToolErrorinstead of silently falling back to the first non-whitespace column. code_actionsusesqueryin two different ways: server-sidecontext.onlyfilter in list mode, client-side title/index selector in apply mode.renameandrename_filedefault to apply. Preview requiresapply: false.requestwithfile: "*"is treated the same as omittedfile: it does not build workspace-specific params.reloaddoes not recreate a client immediately after killing it; the next request triggers reinitialization.workspace/applyEditcan apply edits initiated by the server outside the direct tool action result path.detectLspmux()can be disabled withPI_DISABLE_LSPMUX=1; onlyrust-analyzeris inDEFAULT_SUPPORTED_SERVERS.- Startup LSP discovery (
discoverStartupLspServers(cwd)insdk.ts) runs forenableLsp && options.hasUI; the background warmup additionally requires!settings.get("lsp.lazy").lsp.lazydefaults totrue, so by default discovered servers are surfaced with status"available"(gray dot in the welcome screen) and cold-start throughgetOrCreateClient()on first use (lsp tool call or edit/write on a matching file type). Print/RPC/ACP/script sessions skip discovery and warmup entirely. Seedocs/sdk.md§ Startup performance. configCacheis per-process and never auto-invalidated; config changes require a fresh process to be observed bygetConfig()callers.