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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Each version's section is shown in the in-app update window and on the GitHub release.

## 1.100.1

### Fixed
- **Menew quitting by itself every few minutes.** With automatic Memory on, reading the text of a screenshot that macOS's text recognition can't handle made Menew quit, and it tried the same image again after relaunching. Those images are now saved without text instead.

## 1.100.0

### New
Expand Down
24 changes: 22 additions & 2 deletions Sources/Menew/TextRecognizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ enum TextRecognizer {
/// Linhas reconhecidas, de cima para baixo (vazio se não achou texto)
static func recognize(_ image: CGImage) async -> String {
await withCheckedContinuation { cont in
// Quando falha, o Vision chama o bloco com o erro E ainda lança a exceção: responder duas vezes derruba o app
let once = ResumeOnce(cont)
let request = VNRecognizeTextRequest { req, _ in
let lines = (req.results as? [VNRecognizedTextObservation] ?? [])
.compactMap { $0.topCandidates(1).first?.string }
cont.resume(returning: lines.joined(separator: "\n"))
once.resume(lines.joined(separator: "\n"))
}
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
request.automaticallyDetectsLanguage = true
do {
try VNImageRequestHandler(cgImage: image).perform([request])
} catch {
cont.resume(returning: "")
once.resume("")
}
// Sem erro e sem chamar o bloco (não deveria acontecer): não deixa quem espera parado para sempre
once.resume("")
}
}

Expand Down Expand Up @@ -77,3 +81,19 @@ enum TextRecognizer {
text: lines == 1 ? L("Texto copiado") : L("Texto copiado (%d linhas)", lines))
}
}

/// Responde uma continuação uma vez só; as outras chamadas não fazem nada
final class ResumeOnce<T: Sendable>: @unchecked Sendable {
private var cont: CheckedContinuation<T, Never>?
private let lock = NSLock()

init(_ cont: CheckedContinuation<T, Never>) { self.cont = cont }

func resume(_ value: T) {
lock.lock()
let c = cont
cont = nil
lock.unlock()
c?.resume(returning: value)
}
}
19 changes: 19 additions & 0 deletions Tests/MenewTests/AgentSourcesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,22 @@ final class OtherAgentEventsTests: XCTestCase {
hub.handle(["agent": "cursor", "event": "sessionEnd", "session": "c1"], now: t0)
}
}

final class ResumeOnceTests: XCTestCase {
func testOnlyTheFirstResumeCounts() async {
let v: String = await withCheckedContinuation { c in
let once = ResumeOnce(c)
once.resume("primeiro")
once.resume("segundo") // antes, isto derrubava o app
}
XCTAssertEqual(v, "primeiro")
}

func testRecognizeSurvivesAnImageVisionRejects() async {
// Imagem de 1×1: o Vision recusa, e a leitura devolve vazio em vez de derrubar o app
let ctx = CGContext(data: nil, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4,
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
let text = await TextRecognizer.recognize(ctx.makeImage()!)
XCTAssertEqual(text, "")
}
}
Loading