Skip to content

fix: acp client#16

Merged
sirily11 merged 2 commits into
mainfrom
acp-fix
May 15, 2026
Merged

fix: acp client#16
sirily11 merged 2 commits into
mainfrom
acp-fix

Conversation

@sirily11

Copy link
Copy Markdown
Contributor

No description provided.

Copilot AI review requested due to automatic review settings May 15, 2026 14:49
@vercel

vercel Bot commented May 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rxcode Ready Ready Preview, Comment May 15, 2026 2:49pm

Request Review

@autopilot-project-manager autopilot-project-manager Bot added the bug Something isn't working label May 15, 2026
@sirily11 sirily11 enabled auto-merge (squash) May 15, 2026 14:51
@sirily11 sirily11 merged commit 1e4777b into main May 15, 2026
8 checks passed
@sirily11 sirily11 deleted the acp-fix branch May 15, 2026 14:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors agent backend dispatch, adds ACP session pooling with IDE-side MCP tool bridging, and tightens history/archive filtering in sidebar views with new tests.

Changes:

  • Adds shared backend capability/tool abstractions and AgentBackend adapters for Claude, Codex, and ACP.
  • Introduces an IDE MCP bridge server and ACP MCP server injection for IDE-side tools.
  • Reuses the History sidebar filtering logic in project chat lists and adds archive-filter tests.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
RxCodeTests/HistoryListArchiveFilterTests.swift Adds unit/ViewInspector coverage for archive filtering.
RxCode/Views/Sidebar/ProjectTreeView.swift Reuses shared history filtering for project chat summaries.
RxCode/Views/Sidebar/HistoryListView.swift Exposes shared filter/sort helpers for history summaries.
RxCode/Services/MCPService.swift Builds ACP mcpServers entries, including the IDE bridge.
RxCode/Services/IDEServer/IDEMCPServer.swift Adds local MCP bridge server for IDE-side tools.
RxCode/Services/IDEServer/AppState+IDEToolHandling.swift Implements IDE tool handling through AppState.
RxCode/Services/CodexAppServer.swift Adds AgentBackend conformance for Codex.
RxCode/Services/ClaudeService.swift Adds AgentBackend conformance for Claude.
RxCode/Services/ACPService.swift Refactors ACP transport around pooled sessions and backend conformance.
RxCode/App/AppState.swift Dispatches through AgentBackend and wires ACP IDE MCP setup.
RxCode.xcodeproj/project.pbxproj Adds the new history archive tests to the test target.
Packages/Sources/RxCodeCore/Backend/IDEToolRegistry.swift Adds IDE-side MCP tool descriptors and capability gating.
Packages/Sources/RxCodeCore/Backend/IDEToolHandling.swift Adds IDE tool handler protocol and errors.
Packages/Sources/RxCodeCore/Backend/BackendCapability.swift Adds backend capability model and defaults.
Packages/Sources/RxCodeCore/Backend/AgentBackend.swift Adds unified backend request/protocol abstractions.
Comments suppressed due to low confidence (7)

RxCode/Services/IDEServer/IDEMCPServer.swift:138

  • The listener accepts any loopback connection on a predictable port and immediately binds it to the session without an authentication token. Any local process can scan this 100-port range and call IDE MCP tools such as thread history, usage, or future file/job tools for another session. Include an unguessable per-allocation secret in the bridge command and require it during initialization before serving tools.
    private func accept(connection: NWConnection, port: UInt16) async {
        guard let sessionKey = portIndex[port] else {
            connection.cancel()
            return
        }
        guard let capabilities = allocations[sessionKey]?.capabilities else {
            connection.cancel()
            return
        }

Packages/Sources/RxCodeCore/Backend/IDEToolRegistry.swift:101

  • ide__get_job_output is exposed unconditionally as an IDE-only tool, but the AppState handler currently throws notSupported for this name. Agents will see it in tools/list, call it, and receive an error despite the description promising job output; either implement the handler or omit this descriptor until it works.
        IDETool(
            name: "ide__get_job_output",
            description: "Fetch the tail of a running job's output buffer.",
            visibility: .alwaysIDEOnly,
            inputSchema: .object([

RxCode/Services/IDEServer/AppState+IDEToolHandling.swift:136

  • ide__get_usage ignores the sessionKey and uses the globally selected provider, so a tool call from an ACP session can report Claude/Codex usage if the UI selection has changed or the session has its own provider override. Use the session's agentProvider (falling back to the active selection only when absent) so the usage data matches the backend that made the MCP call.
    private func handleGetUsage() async -> JSONValue {
        let provider = await MainActor.run { selectedAgentProvider }
        let usage = await rateLimitUsage(for: provider, forceRefresh: false)

RxCode/Services/IDEServer/AppState+IDEToolHandling.swift:91

  • An invalid project_id string is converted to nil, which then falls through to the "all projects" path and returns thread metadata across every project. Treat malformed UUIDs as invalid arguments instead of silently widening the scope.
        let projectFilter: UUID? = {
            if let s = arguments["project_id"]?.stringValue { return UUID(uuidString: s) }
            return nil

RxCode/Services/IDEServer/AppState+IDEToolHandling.swift:67

  • Malformed todo entries are silently discarded via compactMap, and the snapshot is still written with the remaining items (or an empty list). A single bad status/missing content from the agent can unintentionally erase existing todos while returning success; validate every entry and throw invalidArguments if any item is malformed.
        let parsed: [TodoItem] = todosArray.enumerated().compactMap { idx, entry -> TodoItem? in
            guard
                let dict = entry.objectValue,
                let content = dict["content"]?.stringValue,
                let statusRaw = dict["status"]?.stringValue,
                let status = TodoItem.Status(rawValue: statusRaw)
            else { return nil }
            let activeForm = dict["activeForm"]?.stringValue ?? content
            return TodoItem(id: idx, content: content, activeForm: activeForm, status: status)

RxCode/Services/IDEServer/IDEMCPServer.swift:303

  • payload is constructed and then discarded before constructing the identical body dictionary. This dead assignment adds noise in a protocol response path and should be removed.
    private func reply(id: Any, result: Any, on connection: NWConnection) async {
        var payload: [String: Any] = [
            "jsonrpc": "2.0",
            "id": id,
            "result": result
        ]
        _ = payload
        let body: [String: Any] = [

RxCode/Services/IDEServer/IDEMCPServer.swift:118

  • Releasing a session only cancels the listener; accepted NWConnections are not stored and therefore cannot be cancelled. Any already-connected MCP bridge can continue to call tools for the released session until it disconnects on its own, which defeats the session-boundary cleanup this method is meant to enforce.
    func release(sessionKey: String) async {
        guard let alloc = allocations.removeValue(forKey: sessionKey) else { return }
        portIndex.removeValue(forKey: alloc.port)
        alloc.listener.cancel()
        Self.logger.info("[IDE] released port \(alloc.port) for session=\(sessionKey, privacy: .public)")

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread RxCode/App/AppState.swift
Comment on lines +2246 to +2247
let idePort = await ideMCPServer.allocate(
sessionKey: sessionKey,
Comment on lines +21 to +27
public func ideAvailableTools(forSession sessionKey: String) async -> [IDETool] {
let provider = await MainActor.run { sessionStates[sessionKey]?.agentProvider } ?? .acp
let caps = await backend(for: provider).capabilities(for: sessionKey)
return IDEToolRegistry.tools(for: caps)
}

public func ideHandleToolCall(
Comment on lines +84 to +96
var alloc = Allocation(
port: candidate,
listener: listener,
sessionKey: sessionKey,
capabilities: capabilities
)
_ = alloc
allocations[sessionKey] = Allocation(
port: candidate,
listener: listener,
sessionKey: sessionKey,
capabilities: capabilities
)
Comment on lines +36 to +59
name: "ide__ask_user",
description: "Ask the user a question and wait for their selection. Use when you need clarification or a decision the user must make.",
visibility: .polyfill(.askUserQuestion),
inputSchema: .object([
"type": .string("object"),
"properties": .object([
"question": .object([
"type": .string("string"),
"description": .string("The question to show the user."),
]),
"options": .object([
"type": .string("array"),
"items": .object(["type": .string("string")]),
"description": .string("2–4 mutually-exclusive options the user can pick from."),
]),
"allow_multiple": .object([
"type": .string("boolean"),
"description": .string("Whether more than one option may be selected."),
]),
]),
"required": .array([.string("question"), .string("options")]),
])
),
IDETool(
),
IDETool(
name: "ide__get_thread_detail",
description: "Fetch the message history of a specific thread by id.",
Comment thread RxCode/App/AppState.swift
Comment on lines +2783 to +2786
/// Release the per-session IDE-MCP listener allocated for ACP turns.
/// Safe to call for non-ACP providers (no-op).
private func releaseIDESession(sessionKey: String) async {
await ideMCPServer.release(sessionKey: sessionKey)
Comment on lines +114 to +119
/// Releases per-stream bookkeeping for a turn that ran to completion.
/// The pooled agent process stays alive so the next turn for the same
/// session inherits its conversation memory.
func finalize(streamId: UUID) {
guard var entry = streams.removeValue(forKey: streamId) else { return }
logger.info("[ACP] finalize streamId=\(streamId.uuidString, privacy: .public) pid=\(entry.process.processIdentifier) pending=\(entry.pending.count) running=\(entry.process.isRunning)")
try? entry.stdin.close()
if entry.process.isRunning {
entry.process.terminate()
guard let key = streamToKey.removeValue(forKey: streamId) else { return }
guard var entry = sessions[key] else { return }
Comment on lines +145 to 172
/// Cancels the in-flight turn for `streamId` via `session/cancel` but
/// keeps the agent process alive so the user can immediately send a new
/// prompt without losing conversation state.
func cancel(streamId: UUID) {
// Attempt graceful cancel via session/cancel notification, then kill the process.
if let entry = streams[streamId], let sid = entry.agentSessionId {
guard let key = streamToKey[streamId], let entry = sessions[key] else {
return
}
if let agentSid = entry.agentSessionId {
let frame: [String: Any] = [
"jsonrpc": "2.0",
"method": "session/cancel",
"params": ["sessionId": sid]
"params": ["sessionId": agentSid]
]
if let data = try? JSONSerialization.data(withJSONObject: frame),
let line = String(data: data, encoding: .utf8) {
_ = try? entry.stdin.write(contentsOf: Data((line + "\n").utf8))
}
}
// Resume any pending requests with .streamClosed so the runTurn await
// unblocks and we hit the per-turn cleanup path in finalize.
mutateSession(key) { e in
for (_, cont) in e.pending {
cont.resume(throwing: ACPError.streamClosed)
}
e.pending.removeAll()
e.continuation?.finish()
}
finalize(streamId: streamId)
Comment on lines +161 to +164
if let bridge = bridgeCommand {
entries.append(.object([
"name": .string("rxcode-ide"),
"command": .string(bridge.command),
Comment thread RxCode/App/AppState.swift
Comment on lines +967 to +972
let codex = self.codex
let ideMCPServer = self.ideMCPServer
Task {
await acp.setPermissionServer(permission)
await codex.setPermissionServer(permission)
await ideMCPServer.setHandler(self)
@sirily11

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 1.3.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants