Skip to content

Step-by-step visual guidance + fix 22 codebase audit issues#120

Open
HalxDocs wants to merge 1 commit into
farzaa:mainfrom
HalxDocs:fix-codebase-audit-22-issues
Open

Step-by-step visual guidance + fix 22 codebase audit issues#120
HalxDocs wants to merge 1 commit into
farzaa:mainfrom
HalxDocs:fix-codebase-audit-22-issues

Conversation

@HalxDocs

@HalxDocs HalxDocs commented Jul 7, 2026

Copy link
Copy Markdown

Summary

This PR adds step-by-step "show me how" visual guidance and fixes 22 issues from a comprehensive codebase audit — crashes, data races, memory leaks, and patterns that would manifest as production bugs. No behavioral changes to existing features.


✨ Step-by-Step Visual Guidance

Claude can return [GUIDE:N] blocks for "how do I..." questions. Each step has an instruction and optional pointing coordinates — the blue cursor buddy flies to the relevant UI element while speaking the instruction.

New: leanring-buddy/StepByStepGuide.swift (166 lines) — GuidanceStep, StepByStepGuide, StepAdvanceCommand, StepByStepGuideParser

CompanionManager.swift: +247 lines — Full lifecycle: startStepByStepGuide, advanceToNextStep, finishStepByStepGuide, cancelStepByStepGuide, executeStep. Per-step state tracking with generation counter for TTS coordination. Preamble TTS with 30s timeout safety.

OverlayWindow.swift: +57 lines — Step progress bar, step instruction overlay, per-step coordinate mapping with bezier-arc cursor flight

Tests: 30+ cases in leanring_buddyTests.swift (+393 lines) — parsing, commands, lifecycle, cancellation, boundary conditions


🔧 22 Audit Fixes

Critical (crash on teardown)

  1. OverlayWindow: use-after-free crashasyncAfter blocks touch @State after view disappears. Added isViewActive guard flag in all 6 timer/asyncAfter callbacks with [weak self].
  2. OverlayWindow: timer leak — Welcome animation Timer not stored. Moved to @State welcomeAnimationTimer, invalidated in onDisappear.
  3. OverlayWindow: fade racefadeOutAndHideOverlay not re-entrant. Added guard !overlayWindows.isEmpty.
  4. ClaudeAPI: proxy URL force-unwrapURL(string:)! crash. Made init? failable.

High (data races, memory leaks, stuck state)

  1. ClaudeAPI: NSLock no defer — Lock stays locked forever if code between lock/unlock throws. Added defer { unlock() }.
  2. ClaudeAPI: URLSession leak — Session tasks continue after deinit. Added deinit { invalidateAndCancel() }.
  3. ClaudeAPI: silent SSE dropstry? JSONSerialization swallows parse errors. Added diagnostic print().
  4. ClaudeAPI: partial text lossonTextChunk sent full accumulated text. Now sends individual deltas.
  5. ElevenLabsTTSClient: force-unwrap + leakinit? + deinit { invalidateAndCancel() }.
  6. BuddyDictationManager: audio thread data raceactiveTranscriptionSession accessed from background without MainActor isolation. Dispatches via Task { @MainActor in }.
  7. BuddyDictationManager: stale callbacks — Old session callbacks overwrite new session state. Added sessionGeneration counter checked by all callbacks.
  8. BuddyDictationManager: reset gapsessionGeneration not incremented in resetSessionState. Fixed.
  9. CompanionManager: stuck voice statecancelStepByStepGuide didn't reset voiceState. Added voiceState = .idle.
  10. CompanionManager: preamble infinite loopwhile isPlaying spins forever. Added 30s timeout.
  11. CompanionManager: unnecessary cancelcancelStepByStepGuide() called unconditionally. Guarded with if isStepByStepActive.

Medium (races, performance, incorrect behavior)

  1. MenuBarPanelManager: stale dismissalasyncAfter(0.3s) fires after panel re-shown. Added panelShowGeneration check.
  2. CompanionResponseOverlay: 60 Tasks/sec — Timer creates Task { @MainActor } per frame. Removed hop — timer already on main runloop.
  3. CompanionScreenCaptureUtility: virtual display crash — Fallback CGRect from CG coords where AppKit coords expected. Changed to guard let nsScreen with skip.

Low

  1. GlobalPushToTalkShortcutMonitor: use-after-free in C callbackCGEventTapCallBack accesses deallocated self. Added isRunning flag checked in callback.

Testing

  • All 30+ step-by-step guide tests pass
  • Existing tests continue to pass (no behavioral changes)
  • No new dependencies
  • Build verified via Xcode (not xcodebuild — preserves TCC permissions)

Notes

  • leanring_buddy typo is intentional/legacy
  • Pre-existing Swift 6 concurrency warnings and deprecated onChange warning in OverlayWindow.swift are not addressed

Feature: step-by-step 'show me how' guidance
- StepByStepGuide.swift: GuidanceStep model, StepByStepGuide parser,
  StepAdvanceCommand enum for voice-controlled step advancement
- CompanionManager.swift: Full guide lifecycle — start, advance, cancel,
  finish. Preamble TTS with timeout safety, per-step voiceState tracking
- OverlayWindow.swift: Step progress bar, step instruction overlay,
  per-step coordinate mapping for cursor pointing
- 30+ tests covering parsing, edge cases, lifecycle states

Fix 22 codebase audit issues:
Critical:
- OverlayWindow use-after-free crash from asyncAfter blocks after view
  teardown — added isViewActive guard flag checked in all 6 timer/asyncAfter
  callbacks. All closures now use [weak self].
- OverlayWindow timer leak — welcome animation timer now stored in @State
  and invalidated in onDisappear.
- OverlayWindow fade race — guard at top of fadeOutAndHideOverlay prevents
  re-entrant animation groups.
- ClaudeAPI proxy URL force-unwrap — init is now failable (init?).

High:
- ClaudeAPI leaked URLSession — added deinit with invalidateAndCancel().
- ClaudeAPI NSLock no defer — added defer { unlock() } right after lock().
- ClaudeAPI silent SSE parse drops — separated UTF-8 vs JSON failure
  logging so malformed events are not silently swallowed.
- ClaudeAPI partial text loss — onTextChunk now sends individual deltas
  instead of full accumulated text.
- ElevenLabsTTSClient force-unwrap + missing deinit — init? + deinit.
- BuddyDictationManager audio thread data race — audio tap callback now
  dispatches to @mainactor via Task { @mainactor in }.
- BuddyDictationManager session cancellation timing — sessionGeneration
  counter; callbacks check it to ignore stale results from preempted
  sessions.
- CompanionManager stuck voice state on step TTS failure — cancelStepByStepGuide
  now sets voiceState = .idle.
- CompanionManager preamble no timeout — 30-second safety timeout on
  while-isPlaying loop.
- CompanionManager unnecessary cancel — guarded with if isStepByStepActive.

Medium:
- MenuBarPanelManager stale click-outside dismissal — panelShowGeneration
  counter; asyncAfter block checks it to avoid hiding a re-shown panel.
- CompanionResponseOverlay redundant Task at 60fps — removed unnecessary
  Task { @mainactor } hop from cursor tracking timer.
- CompanionScreenCaptureUtility NSScreen lookup failure on virtual displays
  — guard let skip instead of incorrect CG→AppKit coordinate fallback.

Low:
- GlobalPushToTalkShortcutMonitor use-after-free in CGEventTap callback
  — isRunning flag checked in the C callback before accessing self.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant