Port LSP client extension from TypeScript to Rust#1
Open
SSFSKIM wants to merge 7 commits into
Open
Conversation
…rkspace - Add §0 errata table to docs/proposals/03-lsp-crate.md reconciling the spec against the real codex-rs APIs (8 points: 4 confirmed, 4 corrected). - Fix inline code that would not compile: ApplyPatchDocSyncHandle newtype for the doc-sync hook (ExtensionData::get needs a concrete Sized type), real ToolSpec::Namespace builder for LspTool::spec(), exact runtime Config location for the lsp_servers field. - Register ext/lsp-client in the workspace members and add the codex-lsp path dependency (crate scaffold to follow). https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
…ig wiring Build sequence steps 1-7 of the codex-lsp port (docs/proposals/03-lsp-crate.md): - Crate scaffold ext/lsp-client (package codex-lsp / lib codex_lsp) + workspace member, codex-lsp path dep, and lsp-types workspace dep. - config: LspServerConfig type, [lsp_servers] on ConfigToml, lsp_servers on the runtime Config (core). - extension-api: ApplyPatchDocSync trait + ApplyPatchDocSyncHandle + FileChange doc-sync seam (no codex-lsp <- codex-core cycle). - transport: hand-rolled Content-Length JSON-RPC framer over child stdio, testable via tokio::io::duplex. - instance: lifecycle state machine, initialize handshake (utf-16, didSave, no workspace/configuration), ContentModified(-32801) retry with 500/1000/2000ms backoff, workspace/configuration -> null[] responder. - diagnostics: severity map, within-batch + cross-turn LRU dedup, severity sort, caps 10/file + 30 total. - manager: first-wins extension routing, lazy start, full-content didChange doc sync, publishDiagnostics wiring, ManagerDocSync ApplyPatchDocSync impl. 22 unit/integration tests pass (cargo test -p codex-lsp). Tool, extension wiring, and the core apply-patch hook follow in subsequent commits. https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
- tool.rs: single namespaced lsp/query tool, 9 operations, 1->0-based position conversion, lazy didOpen with 10MB guard, two-step call hierarchy, git check-ignore (batch 50) location filtering; ToolExposure::Direct, parallel-safe. - format.rs: tolerant formatting of hover/locations/symbols/call-hierarchy from generic JSON (Location vs LocationLink, DocumentSymbol vs SymbolInformation). - schema.rs: schemars input-schema helper mirroring ext/memories. 30 unit tests pass (cargo test -p codex-lsp). https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
- extension.rs: LspExtension implements ThreadLifecycle (build LspManager from config.lsp_servers, skip Exec sessions; store manager + ApplyPatchDocSyncHandle; shutdown on stop), ToolContributor (lsp/query), ContextContributor (inject pushed diagnostics as a developer-policy fragment each turn). install() registers all three. - core/lsp_sync.rs: notify_if_present reads ApplyPatchDocSyncHandle from the thread store and maps AppliedPatchFileChange -> FileChange (no-op when no LSP extension). - apply_patch.rs: rename _ctx -> ctx, forward committed changes to the doc-sync seam. - app-server: register codex_lsp::install + add codex-lsp dependency. 32 codex-lsp tests pass; app-server builds clean. config.schema.json regen + core test run follow. https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
Adds LspServerConfig definition and the lsp_servers property to the generated config schema (just write-config-schema).
- Replace std Mutex .lock().unwrap() with .unwrap_or_else(PoisonError::into_inner) across transport/instance/diagnostics (unwrap_used is denied workspace-wide). - Fix two genuine await_holding_lock bugs: hoist the tokio MutexGuard .take() out of the if-let in transport::shutdown and instance::stop so no guard is held across .await. - diagnostics: sort_by_key for severity; NonZeroUsize without expect. - schema/tool: panic! instead of expect on infallible schema generation. - manager::request_value -> pub(crate) (no longer leaks the pub(crate) LspError). - transport: while-let reader loop; ok_or_else for piped child stdio; map(Vec::len). clippy -p codex-lsp --tests is clean; 32 tests still pass. https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
Adds src/smoke_tests.rs with two #[ignore] tests run on demand:
- tokio_spawn_rust_analyzer_responds: drives tokio::process + manual framing
against a real rust-analyzer, asserting a Content-Length response.
- real_rust_analyzer_hover: spins up rust-analyzer over the full
LspServerInstance/LspTransport stack against a tiny fixture crate, didOpens
the file, and asserts a genuine hover ('pub fn add(a: i32, b: i32) -> i32'),
exercising the initialize handshake and ContentModified retry during indexing.
The fixture sets [lsp_servers].env RUSTUP_TOOLCHAIN so the spawned server uses a
toolchain that has the rust-analyzer component (the repo pins one via
rust-toolchain.toml that may not), which also demonstrates the env config field.
Run: cargo test -p codex-lsp -- --ignored smoke
Both pass; normal suite stays at 32 passed / 2 ignored; clippy clean.
https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
SSFSKIM
pushed a commit
that referenced
this pull request
Jun 20, 2026
## Stack 1. [1 of 3] Support long raw TUI goal objectives - openai#27508 2. [2 of 3] Support long pasted text in TUI goals - openai#27509 3. **[3 of 3] Support images in TUI goals** - this PR ## Why The first two PRs make goal definitions resilient to long text, but `/goal` still dropped image inputs from the composer. That meant a user could attach images while defining a goal and the resulting goal continuation would not have any useful reference to those images. Goal state still persists only objective text, so image inputs need to become paths or URLs that the agent can read later. ## What Changed - Extends TUI `GoalDraft` with local image attachments and remote image URLs. - Copies local goal images through the app-server filesystem layer into the managed goal attachment directory, then rewrites active image placeholders to file references. - Appends unplaced local images and remote image URLs to the objective as referenced image files or URLs. - Preserves goal image metadata through live `/goal` submission and queued `/goal` dispatch. ## Verification - Added goal materialization coverage for local image files and remote image URLs. - Added/updated TUI slash-command coverage showing `/goal` drafts include attached images instead of dropping them. ## Manual Testing - Attached an image by bracketed-pasting its local path into a live `/goal` composer. The `[Image #1]` placeholder became a server-host `image-1.png` reference, copied bytes matched exactly, and no attachment was written under the TUI's local home. - Deleted an image placeholder before submitting a small goal and verified no image was copied. - Attached PNG and JPEG files to the same goal. Placeholder order was preserved as `image-1.png` and `image-2.jpg`, and both remote copies matched their source bytes. - Tried extensionless, malformed-extension, and extension/content-mismatched paths; the composer rejected them as image attachments before goal dispatch rather than creating misleading managed image files. - Combined a local image, a large pasted block, and enough raw text to exceed 4,000 characters. The remote attachment directory contained the image, paste sidecar, and `goal-objective.md`; all embedded references used server-host paths and both payloads matched their sources. - Submitted an image replacement while a goal was active, verified no image was copied before confirmation, then canceled and confirmed the attachment count was unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR ports Claude Code's Language Server Protocol (LSP) client stack from TypeScript to Rust, enabling Codex to drive language servers for code intelligence features (go-to-definition, find-references, hover, diagnostics, etc.).
The implementation mirrors the three-layer architecture from
Claude Code Src/:transport.rs): Async JSON-RPC framer over child process stdio with LSPContent-Lengthframinginstance.rs): Per-server lifecycle management (spawn → initialize → request/notify with transient-error retry)manager.rs): Routing by file extension, document sync, and diagnostic registrytool.rs): Model-facinglsp/querytool exposing nine operations (go-to-definition, find-references, hover, etc.)Key Changes
New Crates & Modules
codex-lspcrate: Complete LSP client implementation with ~1,500 lines of core logictransport.rs(403 lines): JSON-RPC framer with request/response routing and notification dispatchinstance.rs(305 lines): Server lifecycle with initialize handshake and transient-error retry (ContentModified backoff)manager.rs(363 lines): Extension-based routing, document sync, diagnostic batchingtool.rs(394 lines): Nine LSP operations exposed as a single namespaced toolformat.rs(289 lines): Human-readable formatting of LSP results (locations, diagnostics, symbols)diagnostics.rs(219 lines): Passive registry with deduplication, severity sorting, and volume cappingconfig.rs(88 lines): Validated runtime view of server configsschema.rs(37 lines): JSON schema generation for tool inputextension.rs(118 lines): Extension wiring (thread-scoped manager, tool registration, diagnostic injection)Configuration
codex-config::LspServerConfig(config/src/lsp_config.rs): New TOML config struct for[lsp_servers.<name>]table[mcp_servers]shape: command, args, env, extension-to-language mapping, workspace folder, initialization options, timeouts, restart limitscore/config.schema.jsonIntegration Points
ApplyPatchDocSynctrait (extension-api/src/apply_patch_doc_sync.rs): Seam between apply-patch runtime and LSP manager for document sync without creating a dependency cyclelsp_sync.rsincodex-core: Forwards committed file changes to the LSP manager via the trait objectapp-server/src/extensions.rs: Installs the LSP extension alongside other extensionsTesting
transport_tests.rs: JSON-RPC framer overtokio::io::duplexinstance_tests.rs: Server lifecycle and handshakemanager_tests.rs: Extension routing and document synctool_tests.rs: Tool dispatch and operation formattingdiagnostics_tests.rs: Registry deduplication and volume cappingconfig_tests.rs: Config validation and normalizationformat_tests.rs: Location and diagnostic formattingextension_tests.rs: Extension wiringsmoke_tests.rs): Realrust-analyzerintegration (ignored by default, requiresrust-analyzeron PATH)test_support.rs): Shared helpers for framing and mock server scriptingDesign Notes
JsonRpcError::Closed)https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT