v2.0: the great cleanup + the progressive-slowdown fixes - #2
Draft
amitayks wants to merge 5 commits into
Draft
Conversation
Refocus the app on what actually gets used: the window switcher, the four-finger launcher, and clipboard history. The full-featured app is preserved on the v1 branch and the v1.0.0 release (notarized DMG). Removed outright: - The entire on-device AI platform: the GemmaRuntime (MLX) target and its package deps, Sources/ThreeFingerSwitcher/AI/ (executor, agent loop, tool routing, memory, skills, media, fleet, voice/computer-use, background autonomy + audit, parked sessions), the AI command canvas, the notch home zone + timeline, the region picker, the Hub AI pages, ModelManagementView, the .aiCommand item kind, the speakLastResponse action, and 46 AI test suites. - The Files band: Sources/ThreeFingerSwitcher/Files/, FilesBandView / FilesDwellArming / BubbleMorph, HubFilesPage, the recognizer's Files-drill sub-state, the .fileEntry kind, and 11 Files test suites. - 15 AI/Files openspec capability folders + 16 active ai-* change folders (the archive stays as design history). Surgical edits: AppCoordinator (~1,100 lines of wiring out), GestureRecognizer (canvas-resolve / notch-flick / files-drill out), the launcher stack, the Hub, onboarding, AppSettings (45 keys out), GestureBindings (switcher-only), Package.swift/main.swift (no MLX), build-app.sh, release.yml (Xcode pin was MLX-only), Info.plist (5 AI usage strings out), README + CLAUDE.md. Migration: Favorites schema v3 — bands decode items lossily (ContextBand.FailableItem) so records still carrying retired kinds drop those items instead of failing wholesale and being reseeded; the empty seeded "AI" band is removed by sentinel id. swift build clean; swift test: 805 tests, 0 failures. The whole app now builds under plain swift build (no MLX/xcodebuild-only target). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…idge)
The great cleanup, part two. remove-local-ai kept the device link; on
reflection it goes too — remote device syncing is the same drift from
the app's intent (switcher · launcher · clipboard history) as the AI
stack was, and its iOS companion never shipped. The v1 branch /
v1.0.0 release preserve the full-featured app.
Removed:
- Sources/ThreeFingerSwitcher/DeviceLink/ (service, NW/sealing
transports, handshake, ReceiveHUD, the QR-pairing suite)
- The vendored DeviceLinkKit package + its Package.swift products
- Clipboard Link{Inbound,Outbound}Adapter + the coordinator's
receive/auto-paste/send-latest paths
- The Hub Devices page (.devices destination, rail button, seams)
- The enableDeviceLink setting; NSLocalNetworkUsageDescription +
NSBonjourServices from Info.plist
- ClipboardOrigin/.peer provenance + the band's ProvenanceChip
(legacy indexes with an origin key still decode — keys ignored)
- ClipboardMonitor.suppressSelfWrite (only caller was the receive path)
- 9 spec capabilities (device-link-* + mac-qr-pairing); provenance
requirements in clipboard-history replaced with legacy-compat
swift build green; 777 tests pass (was 805 — deleted suites were
device-link-only). Change folder: openspec/changes/remove-device-link.
Story-origin: the second cut of the great cleanup — realizing the
device link was the same scope drift the AI stack was, just wearing
a hardware accessory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app degraded with use — CPU creeping up, switcher previews refreshing slower, the gesture trigger developing a "needs to wake up" lag — clearing only on restart. A four-way audit (repeating-work leaks, event-source lifecycle, unbounded growth, thread/QoS) found independent accumulation bugs plus a scheduling posture that let macOS starve the app under load. Accumulation (the "more use = slower" half): - Vendored OpenMTManager registers its OWN sleep/wake observers; its wake handler double-starts multitouch devices, orphaning one running, callback-registered device per sleep/wake cycle — N wakes meant every touch frame processed N+1 times through one serial queue. TouchEngine now strips those observers at init via the ObjC runtime (fails safe to a no-op); AppCoordinator is the sole owner of sleep/wake restart. - MRUTracker.start()/KeyboardLanguageService.start() were not idempotent and the $enabled sink could ping-pong unbounded in the no-trackpad case (@published emits on every WRITE; enable() re-writes settings.enabled) — each lap stacked a permanent NSWorkspace activation observer. Guards + .removeDuplicates() on both sinks. - Closing the first-run wizard only suspend()ed its model, leaving the retained NSHostingView's ungated 15-20 Hz TimelineView breathers ticking forever (the postmortem-idle-cpu-spin loop, reachable from the red button). willClose now destroys window + model + hosting tree; attract timer drops its strong self. - Preview sweeps stacked: each 0.8 s tick spawned an uncancelled Task, and once a sweep overran the tick each overlap paid a full SCShareableContent enumeration. Sweeps are now single-slot (skip-if-busy) and cancelled on overlay hide. - ThumbnailService cache was FIFO and never dropped closed windows (~1 MB pinned per entry, two 64-slot instances): now true LRU + pruned to the live cross-Space id set each switcher open. - The 1 Hz permissions poll (six TCC/XPC round-trips per tick) could strand forever when the Hub closed on the Setup page (.onDisappear never fires in a retained window): the tick now no-ops with no regular window visible. Load sensitivity (the "can't compete with other apps" half): - Process-global AXUIElementSetMessagingTimeout(0.5 s): AX calls default to a 6 s per-call timeout serviced by the TARGET app's main thread, and the snapshot makes several per window synchronously inline in the gesture. - App Nap opt-out (NSAppSleepDisabled + beginActivity with .userInitiatedAllowingIdleSystemSleep — deliberately NOT .userInitiated, which would block system idle sleep). - Event taps get a 2 s tapIsEnabled watchdog (the in-band re-enable only runs when the NEXT event limps through, dropping the first post-stall gesture) + CFMachPortInvalidate on stop. - snapshot()'s per-app off-Space brute-force sweeps get one 250 ms aggregate deadline (was: unbounded 100 ms x app count, inline in the trigger). - Dock AX walk throttled to an 80 ms TTL cache (was: full Dock.app AX tree walk per mouse-move at 60-125 Hz near any screen edge); the dock defaults handle is reused; the snap monitor stops installing per-move monitors it never consumed. - The launcher rebuilt its whole SwiftUI graph on EVERY open: the panel stays disposable (ghost-on-Space-switch fix untouched) but the NSHostingView is now built once and re-parented. Spec-first: openspec/changes/fix-progressive-cpu-degradation/ (proposal, design with rejected alternatives + deferred follow-ups, spec deltas for touch-input and menubar-app-shell). CLAUDE.md gains a guardrails section so the odd-looking-but-load-bearing bits (runtime removeObserver, the activity option choice) don't get "cleaned up". Verification: swift build clean, swift test 777/777 green. Real-build checks that need the user's stable-signed install (single-processing across a real sleep/wake, trigger latency under CPU load) are listed as task 3.3. Story-origin: the app aged like milk and the villain was plural — five separate leaks all compounding; the smoking gun was a vendored framework double-starting trackpad devices on every wake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…system-wide stuck state Five fresh-lens audits (retain cycles, main-thread I/O, SwiftUI render storms, retry/poll correctness, crash/safety) over the post-first-sweep tree. Everything confirmed is fixed; the full itemized list is in openspec/changes/fix-progressive-cpu-degradation/tasks.md §3. The headline finds: - With Space-row switching on, EVERY switcher open shelled out to /usr/bin/defaults on the main thread (fork + waitpid, 30-100 ms) via isSpaceRowSwitchingEffective — at exactly the moment the overlay should appear. The `&&` short-circuited it away on default installs, which is why it hid. openSwitcher now reads the recognizer's cached gate. - CGWindowID(NSWindow.windowNumber) traps on the <= 0 number of the retained-but-closed Hub — on every switcher commit and preview tick after the Hub had been opened once. One hubWindowID chokepoint. - Unguarded `as! AXUIElement` / `as! AXValue` on cross-process AX data (traps when a misbehaving app's AX server answers with another CF type) → typed axElement/axValue helpers. - currentFingerCount was never reset when the touch stream stopped: a sleep with three fingers down left the scroll tap swallowing EVERY scroll in every app until quit. Reset on disable/sleep/wake-restart plus a 0.5 s staleness guard in the consume predicate. - missionControlOpen latched true when MC was closed any way but ours, so every later commit posted a stray Escape into the user's app and every open floated at screen-saver level. Cleared on regular-app activation, Space change, sleep, hideOverlay. Main-thread cost: clipboard capture no longer walks the whole payload through Data's iterator nor fully decodes every image (bounded sample hash + ImageIO header dims); the browser AX tree-walk re-validates the remembered address field instead of re-walking at 2 Hz; StageManager's cfprefsd round-trip is TTL-cached; launcher/Hub icon lookups go through a process-wide IconCache (stable NSImage identity also stops SwiftUI re-rasterizing every cell per step); FavoritesStore coalesces its per-keystroke full-tree JSON save; blob-backed clipboard previews are memoized; FirstRunStore mirrors its stage in memory (it was a defaults read per touch frame); emptyTrash and script runs no longer hold a thread (terminationHandler, not waitUntilExit). Deferred actions are tokened (Space-settle poll, de-minimize raise, focus recovery, MC-dismiss commit, new-window single-flight, seed retries); WindowFocusTracker's AX source is in .commonModes with add, teardown and deinit agreeing (a mode mismatch there leaves a source pointing at freed memory); the TouchEngine consumer is generation- tagged; observer tokens are held and removed in deinit; the two Swift-6 isolation warnings (KeepAwakeController statics, CursorMonitor conformance) are fixed at the root. Verification: swift build clean; swift test 783/783 (6 new ResourceBoundsTests pin the LRU/prune + MRU eviction). README and CLAUDE.md guardrails updated; spec deltas extended (scroll consumption never outlives the touch stream; no subprocess/disk I/O on the gesture path). Story-origin: asked "what did we miss?" and the answer was a fork() in the gesture path, a crash that waited for you to open Settings once, and a sleep that could eat every scroll on the Mac. Story-thread: continues the progressive-degradation arc — the first sweep found the leaks, this one found the cliffs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-t open against the .app (or the DMG) reports 'rejected · Insufficient Context' on current macOS even for a notarized artifact — the wrong assessment type, not a broken release. Verified against the published v2.0.0 DMG: -t exec → accepted, source=Notarized Developer ID. Story-origin: routine — a verify recipe that cried wolf on a good release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
feat!): removes the on-device AI stack, the Files band, and the iPhone↔Mac device link. The app is the switcher, the launcher (+ clipboard history), Dock previews, and keyboard language.v1.0.0/ thev1branch preserve the full-featured app.2ae9b15): five compounding leaks — the vendored multitouch framework orphaning a trackpad listener per sleep/wake (the smoking gun), non-idempotent observer starts, the wizard's hidden animation clocks, stacked preview sweeps, unbounded caches — plus App Nap opt-out, a 0.5 s Accessibility timeout, and tap watchdogs.b8ea550): a/usr/bin/defaultsfork on the gesture path, two latent crashes (CGWindowID(windowNumber)on the closed Hub;as!on AX data), the sleep-with-fingers-down scroll-eating stuck state, the stale Mission Control latch, tokened deferred actions, and the Swift 6 isolation warnings.Full record:
openspec/changes/fix-progressive-cpu-degradation/(proposal / design / spec deltas / tasks). README + CLAUDE.md guardrails updated.Release
Tag
v2.0.0points at this branch's tip and has been pushed —release.ymlbuilds, signs, notarizes, and publishes the DMG. Merge with a merge commit or fast-forward (not squash) so the tagged commit lands onmain's history.Verification
swift buildclean (no new warnings in touched files)swift test: 783/783🤖 Generated with Claude Code