Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d815ff6
test: retain local image files through terminal delivery
austinywang Sep 15, 2026
68edbe2
fix: keep local image transfer files alive
austinywang Sep 15, 2026
a5b91d2
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 16, 2026
42631af
Fix duplicate Xcode build file IDs after main merge
austinywang Sep 16, 2026
38dcda7
Restore auth observer required by merged mobile runtime
austinywang Sep 16, 2026
b3daf57
Revert "Restore auth observer required by merged mobile runtime"
austinywang Sep 16, 2026
d7ac49c
Revert "Fix duplicate Xcode build file IDs after main merge"
austinywang Sep 16, 2026
5d3616c
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 16, 2026
aed23e6
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 16, 2026
fd5d49d
test: preserve image paste payloads with auxiliary URLs
austinywang Sep 16, 2026
5395d85
fix: prefer copied image payloads over auxiliary URLs
austinywang Sep 16, 2026
e57f6de
ci: route the release delivery guard through the configured runner
austinywang Sep 16, 2026
872f46e
test: isolate App Store lane versions from release bumps
austinywang Sep 16, 2026
7713752
test: recognize mapped pane resize actions in Dock routing audit
austinywang Sep 16, 2026
fe8e7af
test: use a generic flag in cache round-trip fixtures
austinywang Sep 16, 2026
a5ab2bf
test: await causal transport and dashboard events
austinywang Sep 16, 2026
cdb73a1
test: synchronize client config concurrency through request admission
austinywang Sep 16, 2026
c082946
test: cover drag payload priority and bracketed image delivery
austinywang Sep 16, 2026
1b332f7
fix: preserve complete image payloads through terminal drop routing
austinywang Sep 16, 2026
ddf32d3
test: type mock implementations in Bun test declarations
austinywang Sep 16, 2026
9b3b242
test: allow main-actor terminal startup during image delivery capture
austinywang Sep 16, 2026
4ca686d
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 17, 2026
b1ee541
refactor: keep pasteboard context limited to file insertion
austinywang Sep 17, 2026
c08bcd3
test: preserve Finder originals when the pasteboard includes a TIFF p…
austinywang Sep 17, 2026
5373b4a
fix: preserve backing files before decoding Finder paste previews
austinywang Sep 17, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/cmux-tui-release-delivery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ concurrency:
jobs:
delivery:
name: verify registry delivery
runs-on: ubuntu-latest
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
steps:
# Always use the trusted default branch, never workflow_run's source.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ actor IrxRelayCredentialInstaller {
installed.removeAll()
}

/// Waits for the current installation pass, including its retries, to finish.
func waitUntilSettled() async {
await task?.value
}

private func run(id: UUID) async {
defer {
if taskID == id {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ struct IrxLiveQUICTests {
)
)
await retired.connection.close(code: .explicitRedial, origin: .local)
try await Task.sleep(for: .milliseconds(150))
_ = await retired.connection.termination()
#expect(await engine.currentSession() == nil)
#expect(
journal.counterSnapshot()["dial-started"] ?? 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ struct IrxLivenessTests {
let host = try await IrxLivenessTestHost.make(behavior: .delayFirstProbe)
defer { Task { await host.stop() } }
let session = try await host.dial()
let probe = Task { await session.connection.probeLiveness(deadline: .seconds(30)) }
try await waitUntil { await host.probeCount == 1 }
try await session.connection.startClientKeepalive(interval: .milliseconds(10), deadline: .milliseconds(100)) {
await host.recordDeath()
}
try await waitUntil { await host.probeCount == 1 }
await session.connection.setApplicationActive(false)
// Deliberately outlast the cancelled probe deadline. This represents time
// during which iOS is backgrounded and cannot perform application work.
try await Task.sleep(for: .milliseconds(250))
#expect(await probe.value == false)
try await expectControlRoundTrip(on: session, message: "control-survives-suspension")
#expect(host.journal.counterSnapshot()["miss", default: 0] == 0)
#expect(await host.deathCount == 0)
#expect(await host.probeCount == 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ struct IrxRelayCredentialInstallerTests {
@Test func gatedInstallationRetriesWithoutAnotherMintAndStopsAfterInvalidation() async throws {
let native = RelayInstallProbe(failFirst: true)
let gate = IrxRelayCredentialRotationGate()
let retry = IrxAsyncLatch()
let generation = await gate.begin()
let installer = IrxRelayCredentialInstaller(installed: [], journal: IrxLiveTestSupport.journal(),
sleep: { _ in
await native.noteSleep()
try await Task.sleep(for: .milliseconds(100))
await retry.wait()
}, install: { try await native.install($0) })
await installer.replace(with: [credential("old")], ownership: .init(gate: gate, generation: generation))
try await waitUntil { await native.sleepCount == 1 }
await gate.invalidate()
try await Task.sleep(for: .milliseconds(200))
await retry.signal()
await installer.waitUntilSettled()
#expect(await native.tokens == ["old"])

let current = await gate.begin()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ extension TerminalPasteboardService: TerminalClipboardReading {
public func stringContents(from pasteboard: NSPasteboard) -> String? {
let types = pasteboard.types ?? []

if (types.contains(.fileURL) || types.contains(.URL)),
let hasImagePayload = hasImageData(in: pasteboard)
let hasRTFDAttachmentPayload = types.contains(.rtfd)
// Image copies may also advertise a source URL. Resolve their actual
// image/rich-text payload before treating that URL as paste text.
if !hasImagePayload && !hasRTFDAttachmentPayload,
(types.contains(.fileURL) || types.contains(.URL)),
let urls = pasteboard.readObjects(forClasses: [NSURL.self]) as? [URL],
!urls.isEmpty {
return urls
.map { $0.isFileURL ? $0.path.terminalShellEscaped : $0.absoluteString }
.joined(separator: " ")
}

let hasImagePayload = hasImageData(in: pasteboard)
let hasRTFDAttachmentPayload = types.contains(.rtfd)
let plainText = plainTextContents(from: pasteboard)
let parsedHTMLOutcome: HTMLPlainTextParseOutcome?
if hasImagePayload || hasRTFDAttachmentPayload {
Expand Down
12 changes: 7 additions & 5 deletions Sources/DragOverlayRoutingPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,23 +134,25 @@ enum FileDropTextDropController {
panelId: UUID,
hostedView: GhosttySurfaceScrollView,
urls: [URL],
window: NSWindow?
window: NSWindow?,
pasteboard: NSPasteboard? = nil
) -> Bool {
performPanelTextDrop(
workspace: workspace,
panelId: panelId,
focusIntent: .terminal(.surface),
window: window,
insert: {
hostedView.handleDroppedURLs(urls)
hostedView.handleDroppedURLs(urls, pasteboard: pasteboard)
}
)
}

@discardableResult
static func performTerminalFileDrop(
terminal: GhosttyNSView,
urls: [URL]
urls: [URL],
pasteboard: NSPasteboard? = nil
) -> Bool {
guard let workspaceId = terminal.tabId,
let terminalSurfaceId = terminal.terminalSurface?.id,
Expand All @@ -159,15 +161,15 @@ enum FileDropTextDropController {
terminalSurfaceId: terminalSurfaceId,
workspace: workspace
) else {
return terminal.handleDroppedFileURLs(urls)
return terminal.handleDroppedFileURLs(urls, pasteboard: pasteboard)
}
return performPanelTextDrop(
workspace: workspace,
panelId: panelId,
focusIntent: .terminal(.surface),
window: terminal.window,
insert: {
terminal.handleDroppedFileURLs(urls)
terminal.handleDroppedFileURLs(urls, pasteboard: pasteboard)
}
)
}
Expand Down
7 changes: 4 additions & 3 deletions Sources/FileDropOverlayViewHitTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ extension FileDropOverlayView {
return insert(text, into: textView)
}
if let terminal = terminalUnderPoint(windowPoint) {
return insert(urls, into: terminal)
return insert(urls, into: terminal, pasteboard: sender.draggingPasteboard)
}
return false
}
Expand Down Expand Up @@ -235,10 +235,11 @@ extension FileDropOverlayView {
return true
}

private func insert(_ urls: [URL], into terminal: GhosttyNSView) -> Bool {
private func insert(_ urls: [URL], into terminal: GhosttyNSView, pasteboard: NSPasteboard) -> Bool {
FileDropTextDropController.performTerminalFileDrop(
terminal: terminal,
urls: urls
urls: urls,
pasteboard: pasteboard
)
}

Expand Down
58 changes: 34 additions & 24 deletions Sources/GhosttyNSView+PreparedImageTransfer.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AppKit
import Foundation

extension GhosttyNSView {
Expand Down Expand Up @@ -35,33 +36,42 @@ extension GhosttyNSView {
)
return false
}
let onTextCompletion: () -> Void
switch plan {
case .insertText:
onTextCompletion = {
preparedContent.cleanupTransferredTemporaryFiles(
using: GhosttyApp.terminalPasteboard
)
}
case .insertTextSegments(let segments, _):
var remainingSegments = segments.count
onTextCompletion = {
remainingSegments = max(0, remainingSegments - 1)
guard remainingSegments == 0 else { return }
preparedContent.cleanupTransferredTemporaryFiles(
using: GhosttyApp.terminalPasteboard
)
}
case .uploadFiles, .pasteCloudImages:
onTextCompletion = {}
case .reject:
onTextCompletion = {}
}
return executeImageTransferPlan(
plan,
onCancel: onCancel,
onTextCompletion: onTextCompletion
onCancel: onCancel
)
}
}

func handleDroppedFileURLs(_ urls: [URL], pasteboard: NSPasteboard? = nil) -> Bool {
if let pasteboard {
return insertDroppedPasteboard(pasteboard)
}
let dragTypes = NSPasteboard(name: .drag).types ?? []
guard let durableURLs = GhosttyApp.terminalPasteboard.durableDroppedFileURLs(
urls,
sourceIsTransient: PasteboardFileURLReader.hasPromisedFileURLType(
dragTypes
)
) else {
return false
}
return executePreparedImageTransfer(
.fileURLs(durableURLs),
onCancel: {}
)
}

@discardableResult
func insertDroppedPasteboard(_ pasteboard: NSPasteboard) -> Bool {
let prepared = TerminalImageTransferPlanner.prepareSynchronously(
pasteboard: pasteboard,
mode: .drop
)
#if DEBUG
cmuxDebugLog("terminal.imageDrop.prepared surface=\(terminalSurface?.id.uuidString.prefix(5) ?? "nil") " +
"types=\((pasteboard.types ?? []).map(\.rawValue).joined(separator: ",")) prepared=\(prepared.cmuxDebugDescription)")
#endif
return executePreparedImageTransfer(prepared, onCancel: {})
}
}
30 changes: 2 additions & 28 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9408,32 +9408,6 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}
}

func handleDroppedFileURLs(_ urls: [URL]) -> Bool {
let dragTypes = NSPasteboard(name: .drag).types ?? []
guard let durableURLs = GhosttyApp.terminalPasteboard.durableDroppedFileURLs(
urls,
sourceIsTransient: PasteboardFileURLReader.hasPromisedFileURLType(
dragTypes
)
) else {
return false
}
return executePreparedImageTransfer(
.fileURLs(durableURLs),
onCancel: {}
)
}

@discardableResult
fileprivate func insertDroppedPasteboard(_ pasteboard: NSPasteboard) -> Bool {
executePreparedImageTransfer(
TerminalImageTransferPlanner.prepareSynchronously(
pasteboard: pasteboard,
mode: .drop
),
onCancel: {}
)
}


#if DEBUG
Expand Down Expand Up @@ -11789,11 +11763,11 @@ final class GhosttySurfaceScrollView: NSView {
}

/// Handle file/URL drops, forwarding to the terminal as shell-escaped paths.
func handleDroppedURLs(_ urls: [URL]) -> Bool {
func handleDroppedURLs(_ urls: [URL], pasteboard: NSPasteboard? = nil) -> Bool {
#if DEBUG
cmuxDebugLog("terminal.swiftUIDrop surface=\(surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") urls=\(urls.map(\.lastPathComponent))")
#endif
return surfaceView.handleDroppedFileURLs(urls)
return surfaceView.handleDroppedFileURLs(urls, pasteboard: pasteboard)
}

func terminalViewForDrop(at point: NSPoint) -> GhosttyNSView? {
Expand Down
7 changes: 4 additions & 3 deletions Sources/PaneDropContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,16 @@ extension PaneDropContainer {
_ urls: [URL],
context: PaneDropContext,
hostedView: GhosttySurfaceScrollView?,
window: NSWindow?
window: NSWindow?,
pasteboard: NSPasteboard? = nil
) -> Bool {
if let hostedView {
return performPanelTextDrop(
panelId: context.panelId,
focusIntent: .terminal(.surface),
window: window,
insert: {
hostedView.handleDroppedURLs(urls)
hostedView.handleDroppedURLs(urls, pasteboard: pasteboard)
}
)
}
Expand All @@ -292,7 +293,7 @@ extension PaneDropContainer {
focusIntent: .terminal(.surface),
window: window ?? terminalPanel.surface.uiWindow,
insert: {
terminalPanel.hostedView.handleDroppedURLs(urls)
terminalPanel.hostedView.handleDroppedURLs(urls, pasteboard: pasteboard)
}
)
}
Expand Down
82 changes: 82 additions & 0 deletions Sources/PasteboardFileURLReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import AppKit
import Foundation

enum PasteboardFileURLReader {
static let legacyFilenamesPboardType = NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")
static let promisedFileURLPasteboardType = NSPasteboard.PasteboardType(
rawValue: "com.apple.pasteboard.promised-file-url"
)
static let fileURLPasteboardTypes: Set<NSPasteboard.PasteboardType> = [
.fileURL,
legacyFilenamesPboardType,
promisedFileURLPasteboardType,
]

static func hasFileURLType(_ pasteboardTypes: [NSPasteboard.PasteboardType]) -> Bool {
return pasteboardTypes.contains { fileURLPasteboardTypes.contains($0) }
}

static func hasPromisedFileURLType(
_ pasteboardTypes: [NSPasteboard.PasteboardType]
) -> Bool {
pasteboardTypes.contains(promisedFileURLPasteboardType)
}

static func fileURLs(from pasteboard: NSPasteboard) -> [URL] {
var fileURLs: [URL] = []
var didReadPromisedFileURL = false

let objects = pasteboard.readObjects(
forClasses: [NSURL.self],
options: [.urlReadingFileURLsOnly: true]
) ?? []
for object in objects {
if let url = object as? URL, url.isFileURL {
fileURLs.append(url.standardizedFileURL)
}
}

if let paths = pasteboard.propertyList(forType: legacyFilenamesPboardType) as? [String] {
fileURLs.append(
contentsOf: paths
.filter { !$0.isEmpty }
.map { URL(fileURLWithPath: $0).standardizedFileURL }
)
}

if let rawFileURL = pasteboard.string(forType: .fileURL),
let url = URL(string: rawFileURL),
url.isFileURL {
fileURLs.append(url.standardizedFileURL)
}

for item in pasteboard.pasteboardItems ?? [] {
guard let rawPromisedFileURL = item.string(
forType: promisedFileURLPasteboardType
),
let url = URL(string: rawPromisedFileURL),
url.isFileURL else {
continue
}
fileURLs.append(url.standardizedFileURL)
didReadPromisedFileURL = true
}

// A few providers expose the promised value on the pasteboard rather
// than on an individual item. Preserve that legacy representation as
// a fallback after item-level extraction.
if !didReadPromisedFileURL,
let rawPromisedFileURL = pasteboard.string(
forType: promisedFileURLPasteboardType
),
let url = URL(string: rawPromisedFileURL),
url.isFileURL {
fileURLs.append(url.standardizedFileURL)
}

var seen: Set<String> = []
return fileURLs.filter { url in
seen.insert(url.path).inserted
}
}
}
Loading
Loading