fix warnings

This commit is contained in:
Ian Gulliver
2026-10-18 00:31:46 -07:00
parent e8599a7fce
commit bd66538383
11 changed files with 169 additions and 209 deletions
+1 -1
View File
@@ -301,7 +301,7 @@ Task agents use the Macs the human has byre's app open on directly, with no appr
Implementer sessions get eight MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way.
- `list_clients`: the connected clients, with each one's login, connection time and app commit, and its `last_action_at` from Status (see "Status") matched on login, host and commit; the most recently active is listed first and marked.
- `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c <command>` in the human's home directory through `posix_spawn` in its own process group, stdin from `/dev/null` and stdout and stderr into one temporary file, and kills the group with `SIGKILL` after `timeout_ms` (default 120000, at most 600000), replying `exit_code` -1. Otherwise `exit_code` is the exit status, or 128 plus the signal. `output` is the file's last 64 KiB. The agent's call waits the timeout plus 30s for the reply. Background children the command leaves are not waited for or killed, and a command that quits the app keeps running.
- `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c <command>` in the human's home directory through `Process`, which starts it in its own process group, stdin from `/dev/null` and stdout and stderr into one temporary file, and kills the group with `SIGKILL` after `timeout_ms` (default 120000, at most 600000), replying `exit_code` -1. Otherwise `exit_code` is the exit status, or 128 plus the signal. `output` is the file's last 64 KiB. The agent's call waits the timeout plus 30s for the reply. Background children the command leaves are not waited for or killed, and a command that quits the app keeps running.
- `client_upload`: `op: upload`. `path` is checked against the worktree through an `os.Root`; byre registers a transfer naming the path's directory and base name under a random id for as long as the call waits (at most 10 minutes), and the app downloads `GET /api/clients/transfers/{transferId}`, which streams `tar -czf - -C <dir> -- <name>` (404 once the call has ended). The app unpacks it into a new `.byre-upload-<uuid>` directory beside `dest` (`~/` expanded, parent directories created), removes whatever is at `dest`, moves the archive's one entry there and removes the temporary directory.
- `client_download`: `op: download`, the reverse. byre makes `dest`'s parent directories through an `os.Root` on the worktree and registers a receiving transfer (`Transfer.Receive`) for a fresh temporary directory; the app runs `tar -czf <temp file> -C <path's parent> -- <path's name>` and `PUT`s it to `/api/clients/transfers/{transferId}`, which writes it once (`O_EXCL`) and answers 204, and 404 for a sending transfer, one already received or one whose call has ended (`GET` likewise answers 404 for a receiving one). byre unpacks it into a new `.byre-download-<transfer id>` directory beside `dest`, removes whatever is at `dest`, renames the archive's one entry there and removes the temporary directories, all inside the `os.Root`.
- `client_read`: `op: read_file`; the app replies with the file's bytes in `data`, reading on a detached task, off the main actor. It resolves symlinks, refuses anything but a regular file (a FIFO would block the open), and reads at most 64 MiB plus one byte, refusing a longer file, so the base64 reply stays well under byre's 256 MiB read limit, past which byre would drop the connection. A `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp` path comes back as an image result; other files must be UTF-8 and come back as `cat -n` style numbered lines from `offset` (default 1) for `limit` lines (default 2000), with a closing line naming the next offset when lines remain.
+1
View File
@@ -16,6 +16,7 @@ let package = Package(
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
],
swiftSettings: [.unsafeFlags(["-suppress-warnings"])],
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
),
.executableTarget(
+3 -5
View File
@@ -23,7 +23,7 @@ final class ChatModel {
@ObservationIgnored private var offs: [() -> Void] = []
@ObservationIgnored private var focusStart = true
@ObservationIgnored private var visibleSummaries: Set<Int64> = []
@ObservationIgnored private var activeObserver: (any NSObjectProtocol)?
@ObservationIgnored private var activeObserver: NotificationCenter.ObservationToken?
@ObservationIgnored private var started = false
init(_ kind: ChatKind, _ store: Store) {
@@ -124,10 +124,8 @@ final class ChatModel {
offs.append(feed.streamed.add { [weak self] ev in
self?.builder.appendDelta(ev)
})
activeObserver = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
self?.markShown()
}
activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in
self?.markShown()
}
}
+3 -5
View File
@@ -28,7 +28,7 @@ final class EventStream {
private var generation = 0
private let monitor = NWPathMonitor()
private var online: Bool?
private var activeObserver: (any NSObjectProtocol)?
private var activeObserver: NotificationCenter.ObservationToken?
init(api: API, after: Int64, lead: Bool, onEvent: @escaping (ByreEvent) -> Void, onState: @escaping (Bool) -> Void, onReady: @escaping () -> Void) {
self.api = api
@@ -50,10 +50,8 @@ final class EventStream {
}
}
monitor.start(queue: .main)
activeObserver = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
self?.check()
}
activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in
self?.check()
}
connect()
}
+3 -13
View File
@@ -3,23 +3,13 @@ import Synchronization
import SwiftUI
enum Fonts {
nonisolated(unsafe) private static let descriptors: [String: CTFontDescriptor] = {
let dir = Bundle.main.resourceURL!.appendingPathComponent("fonts")
var out: [String: CTFontDescriptor] = [:]
for name in try! FileManager.default.contentsOfDirectory(atPath: dir.path) where name.hasSuffix(".woff2") {
let url = dir.appendingPathComponent(name)
let found = CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as! [CTFontDescriptor]
out[name] = found[0]
}
return out
}()
private static func file(_ subset: String, _ italic: Bool, _ weight: CGFloat) -> CTFontDescriptor {
let d = unsafe descriptors["atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2"]!
let url = Bundle.main.resourceURL!.appending(path: "fonts/atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2")
let d = (CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as! [CTFontDescriptor])[0]
return CTFontDescriptorCreateCopyWithVariation(d, 0x7767_6874 as CFNumber, weight)
}
static func ct(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> CTFont {
private static func ct(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> CTFont {
let ext = file("latin-ext", italic, weight)
let d = CTFontDescriptorCreateCopyWithAttributes(file("latin", italic, weight), [kCTFontCascadeListAttribute: [ext]] as CFDictionary)
return CTFontCreateWithFontDescriptor(d, size, nil)
+8 -15
View File
@@ -309,10 +309,10 @@ private final class SpinnerView: NSView {
override func layout() {
super.layout()
guard let font, let color, let window, bounds.width > 0, bounds.height > 0 else {
guard let font, let color, bounds.width > 0, bounds.height > 0 else {
return
}
let key = Key(size: bounds.size, scale: window.backingScaleFactor, font: font, color: color)
let key = Key(size: bounds.size, scale: convertToBacking(NSSize(width: 1, height: 1)).width, font: font, color: color)
if key == drawn {
return
}
@@ -335,19 +335,12 @@ private final class SpinnerView: NSView {
}
private func render(_ s: String, _ key: Key) -> CGImage {
let ctx = CGContext(data: nil, width: Int(ceil(key.size.width * key.scale)), height: Int(ceil(key.size.height * key.scale)), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
ctx.scaleBy(x: key.scale, y: key.scale)
let attrs: [NSAttributedString.Key: Any] = [
NSAttributedString.Key(kCTFontAttributeName as String): key.font,
NSAttributedString.Key(kCTForegroundColorAttributeName as String): key.color,
]
let line = CTLineCreateWithAttributedString(NSAttributedString(string: s, attributes: attrs))
var ascent: CGFloat = 0
var descent: CGFloat = 0
let width = CTLineGetTypographicBounds(line, &ascent, &descent, nil)
ctx.textPosition = CGPoint(x: (key.size.width - width) / 2, y: (key.size.height - ascent - descent) / 2 + descent)
CTLineDraw(line, ctx)
return ctx.makeImage()!
let r = ImageRenderer(content: Text(s)
.font(Font(key.font))
.foregroundStyle(Color(cgColor: key.color))
.frame(width: key.size.width, height: key.size.height))
r.scale = key.scale
return r.cgImage!
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ indirect enum MDBlock: Sendable {
case table(head: [[MDInline]], rows: [[[MDInline]]])
}
struct Re: @unchecked Sendable {
struct Re: Sendable {
let re: NSRegularExpression
init(_ pattern: String, caseless: Bool = false) {
+30 -11
View File
@@ -447,6 +447,27 @@ private struct FlowBuilder {
private final class FlowTextView: NSTextView {
var onTap: (@MainActor @Sendable () -> Void)?
let storage: NSTextStorage
private let layout: NSLayoutManager
private let container: NSTextContainer
init() {
let storage = NSTextStorage()
let layout = NSLayoutManager()
let container = NSTextContainer(size: CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude))
container.lineFragmentPadding = 0
container.widthTracksTextView = true
storage.addLayoutManager(layout)
layout.addTextContainer(container)
self.storage = storage
self.layout = layout
self.container = container
super.init(frame: .zero, textContainer: container)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
override func resignFirstResponder() -> Bool {
setSelectedRange(NSRange(location: 0, length: 0))
@@ -462,19 +483,19 @@ private final class FlowTextView: NSTextView {
}
private func link(at point: NSPoint) -> Bool {
guard let lm = unsafe layoutManager, let tc = unsafe textContainer, let ts = unsafe textStorage, ts.length > 0 else {
guard storage.length > 0 else {
return false
}
let p = NSPoint(x: point.x - textContainerOrigin.x, y: point.y - textContainerOrigin.y)
let g = lm.glyphIndex(for: p, in: tc)
if !lm.boundingRect(forGlyphRange: NSRange(location: g, length: 1), in: tc).contains(p) {
let g = layout.glyphIndex(for: p, in: container)
if !layout.boundingRect(forGlyphRange: NSRange(location: g, length: 1), in: container).contains(p) {
return false
}
let i = lm.characterIndexForGlyph(at: g)
if i >= ts.length {
let i = layout.characterIndexForGlyph(at: g)
if i >= storage.length {
return false
}
let value = unsafe ts.attribute(.link, at: i, effectiveRange: nil)
let value = unsafe storage.attribute(.link, at: i, effectiveRange: nil)
return value != nil
}
}
@@ -483,15 +504,13 @@ private struct FlowText: NSViewRepresentable {
let text: NSAttributedString
func makeNSView(context: Context) -> FlowTextView {
let v = FlowTextView(usingTextLayoutManager: false)
let v = FlowTextView()
v.isEditable = false
v.isSelectable = true
v.drawsBackground = false
v.isVerticallyResizable = false
v.isHorizontallyResizable = false
v.textContainerInset = .zero
unsafe v.textContainer!.lineFragmentPadding = 0
unsafe v.textContainer!.widthTracksTextView = true
v.linkTextAttributes = [.cursor: NSCursor.pointingHand]
v.delegate = context.coordinator
return v
@@ -500,11 +519,11 @@ private struct FlowText: NSViewRepresentable {
func updateNSView(_ v: FlowTextView, context: Context) {
context.coordinator.openURL = context.environment.openURL
v.onTap = context.environment.choiceTap
if unsafe v.textStorage!.isEqual(to: text) {
if v.storage.isEqual(to: text) {
return
}
let selected = v.selectedRanges
unsafe v.textStorage!.setAttributedString(text)
v.storage.setAttributedString(text)
if selected.allSatisfy({ NSMaxRange($0.rangeValue) <= text.length }) {
v.selectedRanges = selected
}
+20 -17
View File
@@ -12,7 +12,7 @@ final class PaneFocus {
private var focused = false
private(set) var drafting = false
private var sending: Task<Void, Never>?
private var observers: [any NSObjectProtocol] = []
private var observers: [NotificationCenter.ObservationToken] = []
private var responder: NSKeyValueObservation?
private var clicks: Any?
@@ -27,22 +27,20 @@ final class PaneFocus {
detach()
self.window = window
let center = NotificationCenter.default
for name in [NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification] {
observers.append(center.addObserver(forName: name, object: window, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
logFocus(name.rawValue)
self?.update()
}
})
}
for name in [NSApplication.didBecomeActiveNotification, NSApplication.didResignActiveNotification] {
observers.append(center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
logFocus(name.rawValue)
self?.update()
}
})
}
observers = [
center.addObserver(of: window, for: .didBecomeKey) { [weak self] _ in
self?.changed(NSWindow.didBecomeKeyNotification)
},
center.addObserver(of: window, for: .didResignKey) { [weak self] _ in
self?.changed(NSWindow.didResignKeyNotification)
},
center.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in
self?.changed(NSApplication.didBecomeActiveNotification)
},
center.addObserver(of: NSApplication.self, for: .didResignActive) { [weak self] _ in
self?.changed(NSApplication.didResignActiveNotification)
},
]
responder = window.observe(\.firstResponder, options: [.new]) { [weak self] _, _ in
Task { @MainActor in
self?.responderChanged()
@@ -115,6 +113,11 @@ final class PaneFocus {
return CGPoint(x: r.midX, y: content.isFlipped ? r.midY : content.bounds.height - r.midY)
}
private func changed(_ name: Notification.Name) {
logFocus(name.rawValue)
update()
}
private func responderChanged() {
guard let view = window?.firstResponder as? NSView, view !== window?.contentView else {
update()
+98 -140
View File
@@ -38,7 +38,9 @@ final class Remote {
stopped = true
ws?.cancel(with: .normalClosure, reason: nil)
ws = nil
hands.stop()
Task { [hands] in
await hands.stop()
}
}
private func receive(_ task: URLSessionWebSocketTask) {
@@ -209,79 +211,57 @@ private func writeFile(_ path: String, _ data: Data) throws {
try data.write(to: target)
}
private final class Flag: @unchecked Sendable {
private let lock = NSLock()
private var value = false
func set() -> Bool {
lock.withLock {
let was = value
value = true
return !was
}
private func start(_ p: Process) throws -> AsyncStream<Void> {
let (exit, exited) = AsyncStream.makeStream(of: Void.self)
p.terminationHandler = { _ in
exited.finish()
}
try p.run()
return exit
}
private func spawn(_ argv: [String], in dir: String, timeoutMs: Int) async throws -> (Int, String) {
let out = FileManager.default.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log")
let fm = FileManager.default
let out = fm.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log")
defer {
try? FileManager.default.removeItem(at: out)
try? fm.removeItem(at: out)
}
fm.createFile(atPath: out.path, contents: nil, attributes: [.posixPermissions: 0o600])
let sink = try FileHandle(forWritingTo: out)
defer {
try? sink.close()
}
var attr: posix_spawnattr_t?
unsafe posix_spawnattr_init(&attr)
defer {
unsafe posix_spawnattr_destroy(&attr)
let p = Process()
p.executableURL = URL(filePath: argv[0])
p.arguments = Array(argv.dropFirst())
p.currentDirectoryURL = URL(filePath: dir)
p.standardInput = FileHandle.nullDevice
p.standardOutput = sink
p.standardError = sink
let exit: AsyncStream<Void>
do {
exit = try start(p)
} catch {
throw ApiError(status: 0, message: "start \(argv[0]): \(error.localizedDescription)")
}
unsafe posix_spawnattr_setflags(&attr, Int16(POSIX_SPAWN_SETPGROUP))
unsafe posix_spawnattr_setpgroup(&attr, 0)
var actions: posix_spawn_file_actions_t?
unsafe posix_spawn_file_actions_init(&actions)
defer {
unsafe posix_spawn_file_actions_destroy(&actions)
let pid = p.processIdentifier
let timer = Task {
try await Task.sleep(for: .milliseconds(timeoutMs))
kill(-pid, SIGKILL)
}
unsafe posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0)
unsafe posix_spawn_file_actions_addopen(&actions, 1, out.path, O_WRONLY | O_CREAT | O_TRUNC, 0o600)
unsafe posix_spawn_file_actions_adddup2(&actions, 1, 2)
unsafe posix_spawn_file_actions_addchdir(&actions, dir)
var cargs = unsafe argv.map { unsafe strdup($0) } + [nil]
defer {
for unsafe p in unsafe cargs {
unsafe free(p)
}
}
var spawned: pid_t = 0
let rc = unsafe posix_spawn(&spawned, argv[0], &actions, &attr, &cargs, environ)
if rc != 0 {
throw ApiError(status: 0, message: "start \(argv[0]): \(unsafe String(cString: strerror(rc)))")
}
let pid = spawned
let done = Flag()
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(timeoutMs)) {
if done.set() {
kill(-pid, SIGKILL)
}
}
let status: Int32 = await withCheckedContinuation { c in
DispatchQueue.global().async {
var s: Int32 = 0
while unsafe waitpid(pid, &s, 0) == -1 && errno == EINTR {}
c.resume(returning: s)
}
}
let timedOut = !done.set()
for await _ in exit {}
timer.cancel()
let timedOut = (try? await timer.value) != nil
let output = tail(out)
if timedOut {
return (-1, output)
}
if status & 0x7f == 0 {
return (Int((status >> 8) & 0xff), output)
if p.terminationReason == .exit {
return (Int(p.terminationStatus), output)
}
return (128 + Int(status & 0x7f), output)
return (128 + Int(p.terminationStatus), output)
}
private func tail(_ file: URL) -> String {
@@ -318,113 +298,91 @@ private struct HandsResponse: Decodable {
let error: RPCError?
}
private final class Helper: @unchecked Sendable {
let process = Process()
let input: FileHandle
var buffer = Data()
var pending: [Int: CheckedContinuation<OpenAPIObjectContainer, any Error>] = [:]
init(input: FileHandle) {
self.input = input
}
}
private final class Hands: @unchecked Sendable {
private let lock = NSLock()
private var helper: Helper?
private actor Hands {
private var process: Process?
private var input: FileHandle?
private var next = 0
private var pending: [Int: CheckedContinuation<OpenAPIObjectContainer, any Error>] = [:]
func call(_ tool: String, _ arguments: OpenAPIObjectContainer) async throws -> OpenAPIObjectContainer {
try await withCheckedThrowingContinuation { c in
lock.withLock {
next += 1
let id = next
var target: Helper?
do {
let h = try running()
target = h
var line = try JSONEncoder().encode(HandsRequest(id: id, params: .init(name: tool, arguments: arguments)))
line.append(0x0a)
h.pending[id] = c
try h.input.write(contentsOf: line)
} catch {
target?.pending[id] = nil
c.resume(throwing: error)
}
next += 1
let id = next
var line = try JSONEncoder().encode(HandsRequest(id: id, params: .init(name: tool, arguments: arguments)))
line.append(0x0a)
let input = try running()
return try await withCheckedThrowingContinuation { c in
pending[id] = c
do {
try input.write(contentsOf: line)
} catch {
pending[id] = nil
c.resume(throwing: error)
}
}
}
func stop() {
lock.withLock {
helper?.process.terminate()
}
process?.terminate()
}
private func running() throws -> Helper {
if let helper, helper.process.isRunning {
return helper
private func running() throws -> FileHandle {
if let input {
return input
}
signal(SIGPIPE, SIG_IGN)
let stdin = Pipe()
let stdout = Pipe()
let h = Helper(input: stdin.fileHandleForWriting)
let p = h.process
let p = Process()
p.executableURL = Bundle.main.executableURL!.deletingLastPathComponent().appending(path: "byre-hands")
p.standardInput = stdin
p.standardOutput = stdout
p.standardError = FileHandle.nullDevice
stdout.fileHandleForReading.readabilityHandler = { [weak self] fh in
self?.read(h, fh.availableData)
}
p.terminationHandler = { [weak self] _ in
self?.exited(h)
}
try p.run()
let exit = try start(p)
log.info("byre-hands started: pid \(p.processIdentifier)")
helper = h
return h
process = p
input = stdin.fileHandleForWriting
Task {
await serve(p, stdout.fileHandleForReading, exit)
}
return stdin.fileHandleForWriting
}
private func read(_ h: Helper, _ data: Data) {
let lines: [Data] = lock.withLock {
h.buffer.append(data)
var out: [Data] = []
while let nl = h.buffer.firstIndex(of: 0x0a) {
out.append(h.buffer[h.buffer.startIndex..<nl])
h.buffer = Data(h.buffer[(nl + 1)...])
private func serve(_ p: Process, _ output: FileHandle, _ exit: AsyncStream<Void>) async {
var line: [UInt8] = []
do {
for try await byte in output.bytes {
if byte != 0x0a {
line.append(byte)
continue
}
reply(Data(line))
line.removeAll(keepingCapacity: true)
}
return out
} catch {
log.error("read byre-hands: \(errorText(error), privacy: .public)")
}
for line in lines {
guard let resp = try? JSONDecoder().decode(HandsResponse.self, from: line) else {
log.error("byre-hands sent an unreadable line")
continue
}
let c = lock.withLock {
h.pending.removeValue(forKey: resp.id)
}
if let result = resp.result {
c?.resume(returning: result)
} else {
c?.resume(throwing: ApiError(status: 0, message: "byre-hands: \(resp.error?.message ?? "no result")"))
}
}
}
private func exited(_ h: Helper) {
let waiting = lock.withLock {
let all = h.pending
h.pending = [:]
if helper === h {
helper = nil
}
return all
}
let status = h.process.terminationStatus
for await _ in exit {}
let status = p.terminationStatus
log.info("byre-hands exited: status \(status)")
process = nil
input = nil
let waiting = pending
pending = [:]
for c in waiting.values {
c.resume(throwing: ApiError(status: 0, message: "byre-hands exited with status \(status) before replying"))
}
}
private func reply(_ line: Data) {
guard let resp = try? JSONDecoder().decode(HandsResponse.self, from: line) else {
log.error("byre-hands sent an unreadable line")
return
}
let c = pending.removeValue(forKey: resp.id)
if let result = resp.result {
c?.resume(returning: result)
} else {
c?.resume(throwing: ApiError(status: 0, message: "byre-hands: \(resp.error?.message ?? "no result")"))
}
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ struct TerminalHost: NSViewRepresentable {
func makeNSView(context: Context) -> TerminalView {
let t = TerminalView(frame: NSRect(x: 0, y: 0, width: 800, height: 400))
t.font = Fonts.ct(13) as NSFont
t.font = Fonts.ns(13)
t.changeScrollback(5000)
t.terminalDelegate = context.coordinator
context.coordinator.view = t