Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@
repositoryURL = "https://github.com/rxtech-lab/RxAgentSDK.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 1.0.4;
minimumVersion = 1.0.8;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Packages/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Packages/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/nalexn/ViewInspector", from: "0.10.0"),
.package(url: "https://github.com/rxtech-lab/RxAgentSDK.git", .upToNextMinor(from: "1.0.4")),
.package(url: "https://github.com/rxtech-lab/RxAgentSDK.git", .upToNextMinor(from: "1.0.8")),
.package(url: "https://github.com/SDWebImage/libwebp-Xcode", from: "1.5.0"),
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion Packages/Sources/RxCodeChatKit/BubbleStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ extension View {

// MARK: - AnyInsettableShape

private struct AnyInsettableShape: InsettableShape, @unchecked Sendable {
nonisolated private struct AnyInsettableShape: InsettableShape, @unchecked Sendable {
private let _path: (CGRect) -> Path
private let _inset: (CGFloat) -> AnyInsettableShape

Expand Down
6 changes: 3 additions & 3 deletions Packages/Sources/RxCodeChatKit/IMETextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public struct IMETextView: NSViewRepresentable {
var onTextViewReady: ((NSTextView) -> Void)? = nil
/// Thumbnail for the `[ImageN]` chip with the given 1-based index. When it
/// returns an image, the chip draws it in place of its leading bracket.
var chipThumbnail: ((Int) -> NSImage?)? = nil
var chipThumbnail: (@MainActor (Int) -> NSImage?)? = nil

public init(
text: Binding<String>,
Expand All @@ -55,7 +55,7 @@ public struct IMETextView: NSViewRepresentable {
isEditable: Bool = true,
accessibilityIdentifier: String = "chat-input-text-view",
onTextViewReady: ((NSTextView) -> Void)? = nil,
chipThumbnail: ((Int) -> NSImage?)? = nil
chipThumbnail: (@MainActor (Int) -> NSImage?)? = nil
) {
self._text = text
self._isFocused = isFocused
Expand Down Expand Up @@ -238,7 +238,7 @@ nonisolated private func enumerateChipRanges(in text: String, _ body: (NSRange,

fileprivate final class ChipLayoutManager: NSLayoutManager, NSLayoutManagerDelegate, @unchecked Sendable {
/// Set from the main actor; AppKit calls layout and drawing on the main thread.
nonisolated(unsafe) var thumbnail: ((Int) -> NSImage?)?
nonisolated(unsafe) var thumbnail: (@MainActor (Int) -> NSImage?)?

nonisolated override init() {
super.init()
Expand Down
2 changes: 1 addition & 1 deletion RxCode.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1737,7 +1737,7 @@
repositoryURL = "https://github.com/rxtech-lab/RxAgentSDK";
requirement = {
kind = exactVersion;
version = 1.0.7;
version = 1.0.8;
};
};
E6A001002F8A000100000001 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions RxCode.xcodeproj/xcshareddata/xcodecloud/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id" : "78cf6ff7-750c-4249-a8ac-409d691a9453",
"targets" : [
{
"id" : "0E62358B-547B-4F76-8715-D1ECABA7A06A",
"name" : "RxCodeMobile"
}
]
}
3 changes: 3 additions & 0 deletions RxCode/App/AppState+Messaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@
return
}
logger.info("[IDE_SEND_THREAD] record stream completion stream=\(streamId) session=\(sessionId, privacy: .public) error=\(error ?? "<nil>", privacy: .public) assistantChars=\(assistantText.count, privacy: .public)")
if error == nil {
reconcileTaskCompletionLabel(sessionId: sessionId, assistantText: assistantText)
}
let completion = StreamCompletion(
sessionId: sessionId,
assistantText: assistantText,
Expand Down Expand Up @@ -805,4 +808,4 @@
continuation = nil
cont.resume(returning: value)
}
}

Check warning on line 811 in RxCode/App/AppState+Messaging.swift

View workflow job for this annotation

GitHub Actions / swiftlint

File should contain 600 lines or less excluding comments and whitespaces: currently contains 614 (file_length)
19 changes: 19 additions & 0 deletions RxCode/App/AppState+TaskRuns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,25 @@ extension AppState {
}
}

/// Labels a completion-check thread from the response its run actually
/// finished with. The spawn only waits `timeoutSeconds`; a slow check is
/// labelled from partial text that has no `TASK_RESULT:` marker yet, so it
/// reads "Unverified" even after the run goes on to end with COMPLETE.
/// Called for every finished stream; a no-op for other threads and for
/// responses without a verdict.
func reconcileTaskCompletionLabel(sessionId: String, assistantText: String) {
let resolved = resolveCurrentSessionId(sessionId)
guard let summary = allSessionSummaries.first(where: { $0.id == resolved })
?? threadStore.fetch(id: resolved)?.toSummary(),
taskCompletionCheckState(for: summary) != nil,
let parentThreadId = summary.parentThreadId,
let verified = Self.taskCompletionVerdict(from: assistantText)
else { return }
let label = verified ? Self.taskCompletionVerifiedLabel : Self.taskCompletionUnverifiedLabel
guard summary.threadLabel != label else { return }
setTaskCompletionLabel(resolved, parentThreadId: parentThreadId, verified: verified)
}

private func updateTaskAttention(_ id: UUID, reason: String?) {
guard let task = self.task(id: id) else { return }
var board = taskBoard(for: task.projectId)
Expand Down
18 changes: 18 additions & 0 deletions RxCode/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -12910,6 +12910,12 @@
}
}
}
},
"Least recent" : {

},
"Least used" : {

},
"Left in 5h" : {
"localizations" : {
Expand Down Expand Up @@ -14469,6 +14475,12 @@
}
}
}
},
"Most recent" : {

},
"Most used" : {

},
"Most used model" : {
"localizations" : {
Expand Down Expand Up @@ -22741,6 +22753,9 @@
}
}
}
},
"Showing up to %lld tasks per provider" : {

},
"Shown only — nothing is passed back to the session." : {
"localizations" : {
Expand Down Expand Up @@ -23214,6 +23229,9 @@
}
}
}
},
"Sort by" : {

},
"Source" : {
"localizations" : {
Expand Down
41 changes: 1 addition & 40 deletions RxCode/Services/ClaudeService+Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ extension ClaudeCodeServer {

var rawLineCount = 0
var capturedSessionId: String?
for await line in Self.asyncLines(from: stdout.fileHandleForReading, log: log) {
for await line in stdout.fileHandleForReading.lineStream() {
guard !line.isEmpty else { continue }
guard let data = line.data(using: .utf8) else { continue }

Expand Down Expand Up @@ -168,45 +168,6 @@ extension ClaudeCodeServer {
}
}

/// Stream lines from `handle` using a Dispatch-backed `readabilityHandler`.
/// We use this instead of `FileHandle.AsyncBytes.lines` because the async
/// iterator can wedge when multiple concurrent pipe readers exist (the
/// cross-project send case: one CLI is mid-tool-call while another is
/// just starting). Dispatch's readable source delivers each chunk via a
/// per-handle background callback that doesn't share global async state,
/// so a second simultaneous reader is unaffected by the first's progress.
private static func asyncLines(from handle: FileHandle, log: Logger) -> AsyncStream<String> {
AsyncStream { continuation in
// `buffer` is touched only from the readabilityHandler, which Dispatch
// serializes onto a single internal queue per FileHandle — no lock needed.
nonisolated(unsafe) var buffer = Data()
handle.readabilityHandler = { fh in
let chunk = fh.availableData
if chunk.isEmpty {
// EOF — flush any trailing non-terminated line, then finish.
if !buffer.isEmpty, let trailing = String(data: buffer, encoding: .utf8) {
continuation.yield(trailing)
buffer.removeAll(keepingCapacity: false)
}
fh.readabilityHandler = nil
continuation.finish()
return
}
buffer.append(chunk)
while let newlineIdx = buffer.firstIndex(of: 0x0A) {
let lineData = buffer[buffer.startIndex..<newlineIdx]
buffer.removeSubrange(buffer.startIndex...newlineIdx)
if let line = String(data: lineData, encoding: .utf8) {
continuation.yield(line)
}
}
}
continuation.onTermination = { _ in
handle.readabilityHandler = nil
}
}
}

// MARK: - Descendant tracker

/// Start a background poller that periodically snapshots every descendant of `root`
Expand Down
4 changes: 2 additions & 2 deletions RxCode/Services/CodexAppServer+Login.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension CodexAppServer {
]), to: handles.stdin)

var loginId: String?
for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
try Task.checkCancellation()
guard let object = Self.decodeObject(line) else { continue }
if let requestId = Self.idString(object["id"]), object["method"] != nil {
Expand Down Expand Up @@ -66,7 +66,7 @@ extension CodexAppServer {
try Self.writeJSONLine(Self.notification(method: "initialized", params: [:]), to: handles.stdin)
try Self.writeJSONLine(Self.request(id: 2, method: "account/logout", params: .null), to: handles.stdin)

for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
try Task.checkCancellation()
guard let object = Self.decodeObject(line) else { continue }
if let requestId = Self.idString(object["id"]), object["method"] != nil {
Expand Down
11 changes: 7 additions & 4 deletions RxCode/Services/CodexAppServer+Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension CodexAppServer {
try Self.writeJSONLine(Self.notification(method: "initialized", params: [:]), to: handles.stdin)
try Self.writeJSONLine(Self.request(id: 2, method: "account/rateLimits/read", params: .null), to: handles.stdin)

for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
guard let object = Self.decodeObject(line) else { continue }

if let requestId = Self.idString(object["id"]), object["method"] != nil {
Expand Down Expand Up @@ -101,10 +101,13 @@ extension CodexAppServer {
}

func readStderr(_ stderr: Pipe, streamId: UUID) {
// A blocking `readDataToEndOfFile()` here would pin a cooperative
// thread for the whole life of the app server; with a few servers
// alive that starves the pool and every Codex turn stalls.
Task.detached { [weak self] in
let data = stderr.fileHandleForReading.readDataToEndOfFile()
guard let text = String(data: data, encoding: .utf8), !text.isEmpty else { return }
await self?.appendStderr(text, streamId: streamId)
for await line in stderr.fileHandleForReading.lineStream() {
await self?.appendStderr(line + "\n", streamId: streamId)
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions RxCode/Services/CodexAppServer+Summaries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension CodexAppServer {
var activeThreadId: String?
var turnStarted = false

for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
guard let object = Self.decodeObject(line) else { continue }

if let id = Self.idString(object["id"]), object["method"] == nil {
Expand Down Expand Up @@ -93,7 +93,7 @@ extension CodexAppServer {
var activeThreadId: String?
var turnStarted = false

for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
guard let object = Self.decodeObject(line) else { continue }

if let id = Self.idString(object["id"]), object["method"] == nil {
Expand Down Expand Up @@ -202,7 +202,7 @@ extension CodexAppServer {
var activeThreadId: String?
var turnStarted = false

for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
guard let object = Self.decodeObject(line) else { continue }

if let id = Self.idString(object["id"]), object["method"] == nil {
Expand Down
2 changes: 1 addition & 1 deletion RxCode/Services/CodexAppServer+Turn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extension CodexAppServer {
var assistantTextBuffer = ""
var rawLineCount = 0

for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
guard !Task.isCancelled else { break }
rawLineCount += 1
if rawLineCount == 1 {
Expand Down
2 changes: 1 addition & 1 deletion RxCode/Services/CodexAppServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ actor CodexAppServer {
try Self.writeJSONLine(Self.notification(method: "initialized", params: [:]), to: handles.stdin)
try Self.writeJSONLine(Self.request(id: 2, method: "model/list", params: ["includeHidden": .bool(false)]), to: handles.stdin)

for try await line in handles.stdout.fileHandleForReading.bytes.lines {
for await line in handles.stdout.fileHandleForReading.lineStream() {
guard let object = Self.decodeObject(line),
Self.idString(object["id"]) == "2",
let result = object["result"] else { continue }
Expand Down
43 changes: 43 additions & 0 deletions RxCode/Utilities/FileHandle+Lines.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

extension FileHandle {
/// Stream newline-delimited lines using a Dispatch-backed `readabilityHandler`.
///
/// Use this instead of `FileHandle.AsyncBytes.lines` for pipes to long-lived
/// child processes. `AsyncBytes` funnels every iterator through one shared
/// serial IO queue that performs a blocking `read()`: while one reader waits
/// on an idle pipe, every other `bytes` reader in the app starves, so a new
/// agent's replies sit in its pipe and never wake the `for await`. A
/// readability handler gets its own per-handle dispatch source instead.
func lineStream() -> AsyncStream<String> {
AsyncStream { continuation in
// `buffer` is touched only from the readabilityHandler, which Dispatch
// serializes onto a single internal queue per FileHandle — no lock needed.
nonisolated(unsafe) var buffer = Data()
readabilityHandler = { fh in
let chunk = fh.availableData
if chunk.isEmpty {
// EOF — flush any trailing non-terminated line, then finish.
if !buffer.isEmpty, let trailing = String(data: buffer, encoding: .utf8) {
continuation.yield(trailing)
buffer.removeAll(keepingCapacity: false)
}
fh.readabilityHandler = nil
continuation.finish()
return
}
buffer.append(chunk)
while let newlineIdx = buffer.firstIndex(of: 0x0A) {
let lineData = buffer[buffer.startIndex..<newlineIdx]
buffer.removeSubrange(buffer.startIndex...newlineIdx)
if let line = String(data: lineData, encoding: .utf8) {
continuation.yield(line)
}
}
}
continuation.onTermination = { _ in
self.readabilityHandler = nil
}
}
}
}
Loading
Loading