Redline: OneDrive folder transport as alternative to AirDrop (MMDB-2631) #23

Merged
kua-agent merged 20 commits from feat/redline-onedrive-transport-20260905 into main 2026-09-05 06:17:29 +00:00
2 changed files with 31 additions and 10 deletions
Showing only changes of commit 04d1e73532 - Show all commits
+26 -8
View File
@@ -139,6 +139,14 @@ public final class AppModel {
/// lets the LAST choice win instead of applying stale, superseded work. Not
/// `@Observable`-relevant state pure internal bookkeeping, never read by a View.
var reconcileGeneration = 0
/// The MOST RECENT watcher-reconcile Task spawned by chooseTransport/
/// chooseOneDriveFolder, if one is still (or was just) in flight. send() awaits
/// this BEFORE snapshotting transport/folder, so a toggle immediately followed by
/// Send can never race ahead of the reconcile it depends on (the watcher's
/// recordUncommented flag briefly lagging the just-chosen transport, for example).
/// `Task<Void, Never>` never throws; awaiting an already-completed task's `.value`
/// returns immediately. Not `@Observable`-relevant pure internal bookkeeping.
var pendingReconcileTask: Task<Void, Never>?
func rememberLastComposedPDF(_ url: URL) { lastComposedPDFURL = url }
/// True when a last-composed PDF path is known this run, or the newest
@@ -241,19 +249,29 @@ public final class AppModel {
setStatus((error as? ShotdeckError)?.errorDescription ?? "Could not watch the return folder.")
}
let pref = HotkeyPreference.load()
captureHotkey = pref
if !bindCaptureHotkey(pref) {
setStatus("\(pref.displayString) is already used by another app — capture only works from the menu.")
}
let skipSchedule =
// Env-var-flagged self-test/headless runs (PickerSelfTest's phases, PanelSnapshot,
// and the new ShotdeckTests launch-wiring regression test) skip two real-world
// side effects that are unsafe or meaningless in that context: the update-check
// schedule (a real network call), and binding the REAL, process-wide Carbon
// global hotkey which is not safe to exercise in an automated/parallel test
// process (it can collide with ShotdeckCoreTests' own HotkeyCenterCarbonTests
// running in the same test binary) and was never meaningfully exercised by any
// self-test anyway. A real user launch never sets these env vars, so production
// behavior is unchanged.
let isSelfTestRun =
ProcessInfo.processInfo.environment["SHOTDECK_PICKER_SELFTEST"] != nil
|| ProcessInfo.processInfo.environment["SHOTDECK_SNAPSHOT_DIR"] != nil
|| ProcessInfo.processInfo.environment["SHOTDECK_UPDATE_SELFTEST"] != nil
|| ProcessInfo.processInfo.environment["SHOTDECK_ONEDRIVE_SELFTEST"] != nil
|| ProcessInfo.processInfo.environment["REDLINE_SELFTEST_PHASE"] != nil
if !skipSchedule {
let pref = HotkeyPreference.load()
captureHotkey = pref
if !isSelfTestRun, !bindCaptureHotkey(pref) {
setStatus("\(pref.displayString) is already used by another app — capture only works from the menu.")
}
if !isSelfTestRun {
updateChecker.startSchedule()
}
}
+5 -2
View File
@@ -255,6 +255,9 @@ extension AppModel: SettingsWindowPresenting {
/// the live value before every mutating step, so rapid toggling (this function or
/// chooseOneDriveFolder, in any order) always lets the LAST choice win instead of an
/// earlier, superseded call applying its stale folder/flag after a later one already won.
/// The Task's handle is stored in `pendingReconcileTask` so send() can await its
/// completion before snapshotting transport/folder closing the OTHER race, where a
/// toggle is immediately followed by Send before this reconcile has settled.
func chooseTransport(_ value: SendTransport) {
guard !isSending else {
setStatus("Finish the current send first.")
@@ -274,7 +277,7 @@ extension AppModel: SettingsWindowPresenting {
reconcileGeneration += 1
let generation = reconcileGeneration
Task {
pendingReconcileTask = Task {
guard generation == self.reconcileGeneration else { return }
await watcher.setRecordUncommented(value == .airDrop)
guard generation == self.reconcileGeneration else { return }
@@ -306,7 +309,7 @@ extension AppModel: SettingsWindowPresenting {
reconcileGeneration += 1
let generation = reconcileGeneration
Task {
pendingReconcileTask = Task {
guard generation == self.reconcileGeneration else { return }
do {
try await watcher.updateWatchFolder(url)