# Project export: CLearn

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: We are the first interactive learning layer for personal computing, bridging the digital divide from inside the OS.
- Devpost: https://devpost.com/software/clearn
- GitHub: https://github.com/ttorynn/Clearn
- Video: https://www.youtube.com/embed/NTnS3bXVMcc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Toryn Thompson (1 commits)

## Devpost submission (written by the team)

### Inspiration

As a First-Generation Low Income Student (FGLI), there has been no greater epidemic growing up than that of the digital divide. Be it my Spanish speaking grandparents facing a language barrier or my equally poor class mates living through a tech barrier, the essence of ‘Digital Equity' was established for the marginalized communities that surrounded me. However, within the ever-racing tech space, the people asking questions are no longer just those who lack opportunities, it’s everybody. Gen Z’ers overwhelmed by software bloat, workers adapting to new corporate tools, adults terrified of “breaking things,” realizing this is what initially inspired me. After discussing the topic with my teammate Toryn, we started looking into the greatest issue cases of friction within the act of asking for help, and we found it in the very tool meant to do so: FAQs. Think of that one Aunt or your younger cousin, both of whom have asked you for help on their computer. They might’ve struggled to understand the FAQ page for their problem, or more likely, they just never knew how to find it; that’s who we’re building for.

### What it does

Our model is simple: teach locally, learn globally. While simply holding down “Ctrl + L,” users can speak to their device and ask how to do something, with a transcription of what they are saying appearing in the top-right of their screen. After they finish speaking, their dictated question is considered by an LLM that finds the correct course of action to achieve their goal with the device. These instructions are read out step-by-step via speech synthesis while simultaneously being displayed in the same upper-right window. Once the instructions have finished being spoken, users will be asked if they would like CLearn to show them how to “do” the answer. After pressing a button, CLearn will take passive control of the user’s computer, moving the cursor to appropriate locations and clicking as it re-walks the user through the right steps. This way, users are not simply presented with what they can already find online: instructions they do not understand. Instead, they can learn with a real-world example that allows them to later replicate that same action again, and get an overall better grasp of where things are on their computer.

### How we built it

Clearn is a native macOS application built in Swift using primarily the Cocoa, SwiftUI, and AppKit libraries. It replicates the frosted glass UI seen on many macOS applications by using native styling APIs, and integrates into macOS in a way that is necessary for many of its functionalities, such as CLearn’s ability to overlay all other applications. Behind the scenes, CLearn uses Claude’s latest models to generate the instructions for the task that the user requested and to process screenshots of the user's screen for computer control/cursor movement. Additionally, it employs Fish Audio to generate the text-to-speech voice that users hear when their instructions are being read to them. CLearn is different than traditional macOS apps as it runs entirely in the background, so logic was implemented to ensure that CLearn only appears as an icon in the menu bar while it is running—unless, of course, the user is actively using it. As with most macOS apps, CLearn was built entirely in XCode.

### Challenges we ran into

Several challenges were faced during CLearn’s creation, the most prominent being Swift’s very opinionated syntax and data processing methods. Our two-person team knew nothing about macOS development going into the hack, and Swift’s difficult methods of doing even simple things, such as external API requests made it a consistent pain to work with. Apple’s documentation is not exactly friendly for beginners, either, with many of the docs not including extensive examples that make it clear. It seems even AI models such as GPT-5 have a hard time grasping Swift, and when attempting to use these to speed up development, we consistently ran into issues with the AIs either using iOS-only APIs, or simply not providing correct code at all. Moreover, creating UIs in macOS—especially the advanced overlay employed for CLearn—is not an easy task; it is very different from the web development that one of two people on our team was familiar with, making past experience not useful. This all begs the question: why macOS development? Well, for two reasons. 1.) A large buying factor for MacBooks is their ‘ease of use,’ in comparison to other competitors like Linux for example. We thought, given our impact vision, that it would best to first target those who, generally speaking, are already inclined to our mission of facilitating capability. 2.) MacBooks are the most popular laptop among college students, and with a growing market of individuals graduating high school having only used Chromebooks—which are not appropriate for college—tech illiteracy proportions on these devices are only growing, especially as Apple adds new features to macOS and changes simple UIs to more complex ones. Thus, we chose macOS because it hosts most of our target audience.

### What we learned

From a technical/building perspective, we learned a lot about building MacOS apps, especially in regard to rapidly using swift/swiftui within the time case of the 'hacking.' Yet, in juggling this, we also learned a lot about the viability of our limitations... as first time hackers we overestimated how long we could stay up for and crashed on day 2 but pulled another all-nighter after waking up. All in all, the actual experience of the hackathon was a blast, the only thing we'd change for next year's Cal Hacks, is bringing along more friends to build with us! From a holistic perspective though, the idea is actually a great one, but, once again, the limitations as a two-man beginner-hack team means we have the opportunity to continue improving our product till it matches the potential impact we learned about. Take for example, in the current labor market, approximately 92% of jobs in the U.S. require digital skills. However, one-third of Americans lack the basic digital skills necessary to execute them. With a stronger educational emphasis, CLearn could be tailored to act as an intermediary step for people to build technical understanding through hand-held guidance. The best part about expansive features like this, is that the assistance is not just in demand from the user’s perspective, but also from larger entities. Another example can be seen from a disability accessibility POV, ADA Title III lawsuits for digital access continue to grow (8,800 in 2024, +7% year over year), further pressuring organizations and businesses to adopt assistive solutions. Meanwhile, broadband/inclusion investments by the U.S. government are predicted to contribute up to $127 billion to GDP over the next 5 years; if an assistive-learning digital-skills product captured just 1% of that GDP impact, that’s $1.2 billion in value within a market that has yet to be innovated in. With almost unlimited paths for successful expansion, I am certain to have learned through CLearn that the world was built by those that removed the barriers to knowledge.

### What's next

: Data analytics & scale. CLearn not only has the potential to guide users within all operating systems, but the ability to guide decisions within communities, companies, and entire governments in fighting the digital divide together. This could be seen in the aggregation and influence of anonymized usage patterns or potentially even the world’s first “Digital Confidence Index” which would measure how well populations understand their devices. However, fully shipped out, the data acquired by CLearn would also be invaluable due to the fact that it primarily targets the people who least know how to use their devices and would previously provide the least data. Hence, in a competitive market, friction and innovation doesn’t just dictate the future of companies, it ripples through entire countries.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 47 KB.
- Swift (language) — detected in the code

## Codebase structure (from repository index)

### Files (12 of 12)

```
AppDelegate.swift
Assets.xcassets/AccentColor.colorset/Contents.json
Assets.xcassets/AppIcon.appiconset/Contents.json
Assets.xcassets/Contents.json
ClaudeManager.swift
ClearnApp.swift
DictationManager.swift
Info.plist
KeyConstant.swift
PopupViewController.swift
ScreenCapturer.swift
SpeechManager.swift
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Init

## Key source files (fetched from GitHub, selected and truncated for size)

### KeyConstant.swift

```swift
//
//  KeyConstant.swift
//  Clearn
//
//  Created by Zahtec on 10/24/25.
//

import AppKit
import KeyboardShortcuts

extension KeyboardShortcuts.Name {
	static let beginRecording = Self("beginRecording", default: .init(.q, modifiers: [.option]))
}

```

### ClearnApp.swift

```swift
//
//  ClearnApp.swift
//  Clearn
//
//  Created by Zahtec on 10/24/25.

import KeyboardShortcuts
import SwiftUI

@main
struct ClearnApp: App {
	@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

	var body: some Scene {
		Settings {
//			EmptyView()
			Form {
				KeyboardShortcuts.Recorder("Toggle Unicorn Mode:", name: .beginRecording)
			}
		}
	}
}

```

### ScreenCapturer.swift

```swift
//
//  ScreenCapturer.swift
//  Clearn
//
//  Created by Zahtec on 10/26/25.
//

import Cocoa
import CoreImage
import ScreenCaptureKit

final class ScreenCapturer {
	private var stream: SCStream?
	private let sampleQueue = DispatchQueue(label: "com.example.screencapture.samplequeue")
	private var continuation: CheckedContinuation<NSImage, Error>?
	private let ciContext = CIContext(options: nil)

	enum CaptureError: Error {
		case noDisplay
		case noImageBuffer
		case imageCreationFailed
	}

	func captureScreenshotOfMainDisplay() async throws -> CGImage {
		let content = try await SCShareableContent.current
		guard let display = content.displays.first else {
			throw CaptureError.noDisplay
		}

		// Capture the entire display. To capture a specific window, use SCContentFilter(window: ...).
		let filter = SCContentFilter(display: display, excludingApplications: [], exceptingWindows: [])

		let config = SCStreamConfiguration()
		// Use the display’s native resolution.
		config.width = display.width
		config.height = display.height
		// BGRA is easy to turn into an NSImage.
		config.pixelFormat = kCVPixelFormatType_32BGRA
		// Optional: pick a color space; sRGB is broadly safe.
		config.colorSpaceName = CGColorSpace.sRGB
		// We only need video for a screenshot.
		config.capturesAudio = false

		let screenshot = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config)

		return screenshot
	}
}

```

### SpeechManager.swift

```swift
//
//  SpeechManager.swift
//  Clearn
//
//  Created by Zahtec on 10/25/25.
//

import AVFoundation
import Foundation

class SpeechManager: NSObject, AVAudioPlayerDelegate {
	private var audioPlayer: AVAudioPlayer?
	private var playbackCallback: (() -> Void)?
	private let apiKey = ""

	func textToSpeech(text: String, completion: @escaping (Result<Data, Error>) -> Void) {
		let url = URL(string: "https://api.fish.audio/v1/tts")!
		var request = URLRequest(url: url)
		request.httpMethod = "POST"
		request.setValue("Bearer \(self.apiKey)", forHTTPHeaderField: "Authorization")
		request.setValue("application/json", forHTTPHeaderField: "Content-Type")
		request.setValue("s1", forHTTPHeaderField: "model")

		let parameters: [String: Any] = [
			"text": text,
			"format": "mp3",
			"latency": "balanced",
			"reference_id": "933563129e564b19a115bedd57b7406a",
		]

		request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)

		let task = URLSession.shared.dataTask(with: request) { data, response, error in
			if let error = error {
				completion(.failure(error))
				return
			}

			guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
				let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
				let error = NSError(domain: "FishAudioAPI", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Invalid response from server"])
				completion(.failure(error))
				return
			}

			if let data = data {
				completion(.success(data))
			}
		}

		task.resume()
	}

	func playAudio(data: Data, completion: @escaping () -> Void) {
		do {
			self.audioPlayer = try AVAudioPlayer(data: data)
			self.audioPlayer?.delegate = self
			self.audioPlayer?.prepareToPlay()
			self.audioPlayer?.play()

			self.playbackCallback = completion
		} catch {
			print("Error playing audio: \(error.localizedDescription)")
		}
	}

	func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) {
		self.playbackCallback?()
	}

	func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error: Error?) {
		if let error = error {
			print("Audio decoding error: \(error.localizedDescription)")
		}
	}
}

```

### DictationManager.swift

```swift
//
//  DictationManager.swift
//  VoiceApp
//
//  Created by Zahtec on 10/24/25.
//
// SpeechManager.swift

import AVFoundation
import Foundation
import Speech

class DictationManager {
	// MARK: Private properties
	private let audioEngine = AVAudioEngine()
	private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
	private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
	private var recognitionTask: SFSpeechRecognitionTask?
	private var isTranscribing = false

	// MARK: Permissions
	// Request both Speech Recognition and Microphone permissions
	func requestPermissions() {
		SFSpeechRecognizer.requestAuthorization { _ in }

		// macOS microphone permission via AVCaptureDevice
		AVCaptureDevice.requestAccess(for: .audio) { _ in }
	}

	func startRecording(onPartialResult: @escaping (String) -> Void,
	                    onFinalResult: @escaping (String) -> Void,
	                    errorOccurred: @escaping (String) -> Void) throws
	{
		guard !self.isTranscribing else { return }
		guard let speechRecognizer = speechRecognizer, speechRecognizer.isAvailable else {
			throw NSError(domain: "SpeechTranscriber", code: 1, userInfo: [NSLocalizedDescriptionKey: "Speech recognizer is not available."])
		}

		// Cancel any previous task cleanly
		self.cancelRecognition()

		let request = SFSpeechAudioBufferRecognitionRequest()
		request.shouldReportPartialResults = true
		request.taskHint = .dictation
		request.requiresOnDeviceRecognition = false

		self.recognitionRequest = request

		self.recognitionTask = speechRecognizer.recognitionTask(with: request) { [weak self] result, error in
			guard let self = self else { return }

			if let error = error {
				// On error, cancel and clean up
				self.cancelRecognition()
				errorOccurred(error.localizedDescription)
				return
			}

			guard let result = result else { return }

			let text = result.bestTranscription.formattedString

			if result.isFinal {
				onFinalResult(text)
				// Final result arrived – clean up without canceling
				self.cleanupAfterFinal()
			} else {
				onPartialResult(text)
			}
		}

		// Configure audio engine and begin capturing mic input
		let inputNode = self.audioEngine.inputNode
		let recordingFormat = inputNode.outputFormat(forBus: 0)

		inputNode.removeTap(onBus: 0)
		inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in
			self?.recognitionRequest?.append(buffer)
		}

		self.audioEngine.prepare()
		try self.audioEngine.start()
		self.isTranscribing = true
	}

	// Call this when the user is done speaking and you want a final result.
	func stopAndFinalize() {
		guard self.isTranscribing else { return }
		self.isTranscribing = false

		// End audio so the recognizer can produce a final result.
		self.recognitionRequest?.endAudio()

		// Stop feeding audio; do NOT cancel the task here.
		if self.audioEngine.isRunning {
			self.audioEngine.stop()
		}
		self.audioEngine.inputNode.removeTap(onBus: 0)
	}

	// Use this when you truly want to abort (e.g., before starting a new task or on error).
	func cancelRecognition() {
		self.isTranscribing = false

		self.recognitionRequest?.endAudio() // optional; safe to call
		self.recognitionTask?.cancel()

		self.recognitionTask = nil
		self.recognitionRequest = nil

		if self.audioEngine.isRunning {
			self.audioEngine.stop()
		}
		self.audioEngine.inputNode.removeTap(onBus: 0)
	}

	// Internal: cleanup after a final result without canceling
	private func cleanupAfterFinal() {
		self.recognitionTask = nil
		self.recognitionRequest = nil

		if self.audioEngine.isRunning {
			self.audioEngine.stop()
		}
		self.audioEngine.inputNode.removeTap(onBus: 0)
		self.isTranscribing = false
	}
}

```

### ClaudeManager.swift

```swift
//
//  ClaudeManager.swift
//  VoiceApp
//
//  Created by Zahtec on 10/24/25.
//

import Foundation
import ScreenCaptureKit

// Represents an error that can occur when interacting with the Claude API.
enum ClaudeAPIError: Error {
	case invalidURL
	case requestFailed(Error)
	case invalidResponse
	case decodingError(Error)
}

// Represents a message in a conversation with the Claude model.
struct Message: Codable {
	let role: String
	let content: String
}

struct SpecialMessage: Codable {
	let role: String
	let content: [Content]

	struct Content: Codable {
		let type: String
		let text: String?
		let source: Source?

		struct Source: Codable {
			let type: String
			let file_id: String
		}
	}
}

struct Thinking: Codable {
	let type: String
	let budget_tokens: String
}

// Represents the request payload to be sent to the Claude API.
struct ClaudeAPITextRequest: Codable {
	let model: ClaudeModel
	let max_tokens: Int
	let messages: [Message]
	let thinking: Thinking?
}

struct ClaudeAPIImageRequest: Codable {
	let model: ClaudeModel
	let max_tokens: Int
	let messages: [SpecialMessage]
	let thinking: Thinking?
}

// Represents the response received from the Claude API.
struct ClaudeAPIResponse: Codable {
	let id: String
	let type: String
	let role: String
	let content: [Content]

	struct Content: Codable {
		let type: String
		let text: String?
	}
}

struct ClaudeFileResponse: Codable {
	let id: String
	let type: String
	let filename: String
	let mime_type: String
	let size_bytes: Int
	let created_at: String
	let downloadable: Bool
}

enum ClaudeModel: String, Codable {
	case claudeOpus4_1 = "claude-opus-4-1"
	case claudeSonnet4_5 = "claude-sonnet-4-5"
	case claudeHaiku4_5 = "claude-haiku-4-5"
	case claude3Opus = "claude-3-opus-20240229"
	case claude3Sonnet = "claude-3-sonnet-20240229"
	case claude3Haiku = "claude-3-haiku-20240307"
}

// A client for interacting with the Claude API.
class ClaudeManager {
	private let textRegex = try! NSRegularExpression(pattern: "^[ \t]*```[ \t]*(?:\r?\n)(.*?)(?:\r?\n)?^[ \t]*```[ \t]*", options: [.dotMatchesLineSeparators, .anchorsMatchLines, .caseInsensitive])
	private let apiKey = ""
	private let apiURL = URL(string: "https://api.anthropic.com/v1/messages")!

	// Sends a message to the Claude API and returns the response.
	func sendPrompt(
		prompt: String,
		completion: @escaping (Result<ClaudeAPIResponse, ClaudeAPIError>) -> Void
	) {
		var request = URLRequest(url: apiURL)
		request.httpMethod = "POST"
		request.setValue(self.apiKey, forHTTPHeaderField: "x-api-key")
		request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
		request.setValue("application/json", forHTTPHeaderField: "content-type")

		let requestBody = ClaudeAPITextRequest(
			model: .claudeSonnet4_5,
			max_tokens: 2048,
			messages: [
				.init(role: "user", content: "SYSTEM: The message following this is a question asked by a user about their macOS operating system. They are figuring out how to use their Mac, and need help navigating it. You are to directly answer the question in simple, followable steps that are understandable by a tech novice. Ensure these steps are formatted in a bullet point style using dashes (\"-\"), and are outputted in a code block. If the user does ask about a specific action related to macOS, ask them to be more specific in their request, and do not put any output in backticks. Give only one solution to the question raised. Do not provide a bullet-pointed answer unless specifically asked a question about how to do something on macOS. Overall, do not respond to this part of the prompt, and simply output the steps in the above format without any additional text output/labeling; there should only be code blocks. An example is provided below, do not react to this example, only the actual prompt from the user labeled \"PROMPT:\""),
				.init(role: "user", content: "How do I change the default paper size for my printers?"),
				.init(role: "assistant", content: """
						```
						- Open Settings
						- Navigate to \"Printers & Scanners\"
						- Modify \"Default paper size\"
						```
				"""),
				.init(role: "user", content: "PROMPT: \(prompt)"),
			],
			thinking: nil
		)

		do {
			request.httpBody = try JSONEncoder().encode(requestBody)
		} catch {
			completion(.failure(.decodingError(error)))
			return
		}

		let task = URLSession.shared.dataTask(with: request) { data, _, error in
			if let error = error {
				completion(.failure(.requestFailed(error)))
				return
			}

			guard let data = data else {
				completion(.failure(.invalidResponse))
				return
			}

			do {
				let decodedResponse = try JSONDecoder().decode(ClaudeAPIResponse.self, from: data)
				completion(.success(decodedResponse))
			} catch {
				completion(.failure(.decodingError(error)))
			}
		}

		task.resume()
	}

	func sendScreenshot(image: CGImage, instruction: String, completion: @escaping (Result<ClaudeAPIResponse, any Error>) -> Void) {
		var req = URLRequest(url: self.apiURL)
		req.httpMethod = "POST"
		req.setValue(self.apiKey, forHTTPHeaderField: "x-api-key")
		req.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
		req.setValue("application/json", forHTTPHeaderField: "content-type")

		do {
			let jsonString = """
				   {
					   "model": "claude-opus-4-1",
						   "max_tokens": 5000,
						   "thinking": {
							   "type": "enabled",
							   "budget_tokens": 3000
						   },
						   "messages": [
							   {
								   "role": "user",
									   "content": [
										   {
											   "type": "image",
												   "source": {
													   "type": "base64",
														   "media_type": "image/png",
														   "data": "\(cgImageToBase64(image)!)"
												   }
										   },
										   {
											   "type": "text",
			  "text": "I am a macOS user who wants to do the following task: \\"\(instruction.replacingOccurrences(of: "\"", with: ""))\\". I have attached a screenshot of my screen in its native resolution. Please tel
[truncated — 2078 more characters]
```

### AppDelegate.swift

```swift
//
//  AppDelegate.swift
//  Clearn
//
//  Created by Zahtec on 10/24/25.

import AppKit
import Cocoa
import CoreGraphics
import Darwin
import KeyboardShortcuts

class AppDelegate: NSObject, NSApplicationDelegate {
	private var statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
	private var isRecording = false
	private let textRegex = try! NSRegularExpression(pattern: "^[ \t]*```[ \t]*(?:\r?\n)(.*?)(?:\r?\n)?^[ \t]*```[ \t]*", options: [.dotMatchesLineSeparators, .anchorsMatchLines, .caseInsensitive])
	private let bulletRegex = try! NSRegularExpression(pattern: "^[ \t]*-[ \t]+(.+(?:\r?\n(?![ \t]*- ).+)*)", options: [.anchorsMatchLines, .caseInsensitive])

	private let dictationManager = DictationManager()
	private let claudeManager = ClaudeManager()
	private let speechManager = SpeechManager()
	private var popupViewController: PopupViewController?
	private let screenCapturer = ScreenCapturer()

	func applicationDidFinishLaunching(_: Notification) {
		NSApp.setActivationPolicy(.accessory)

		// Request permissions
		self.dictationManager.requestPermissions()

		// Set up the menu bar item
		self.buildMenu()

		// Set up global hotkey
		self.setupHotkeyMonitor()
	}

	// Menu bar setup
	private func buildMenu() {
		let image = NSImage(systemSymbolName: "cursorarrow.click", accessibilityDescription: "cursorarrow.click")
		self.statusItem.button?.image = image

		let menu = NSMenu()
		menu.addItem(NSMenuItem(title: "Quit Clearn", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "Q"))

		self.statusItem.menu = menu
	}

	// Hotkey logic
	private func setupHotkeyMonitor() {
		KeyboardShortcuts.onKeyUp(for: .beginRecording, action: self.handleKeyUp)
		KeyboardShortcuts.onKeyDown(for: .beginRecording, action: self.handleKeyDown)
	}

	private func handleKeyDown() {
		guard !self.isRecording else { return }
		self.isRecording = true

		// Close any old popup and create a new one for this session.
		self.popupViewController?.close()
		self.popupViewController = PopupViewController()
		self.popupViewController?.show()

		do {
			try self.dictationManager.startRecording(
				onPartialResult: { [weak self] partialText in
					// Feed the live transcription to the popup
					DispatchQueue.main.async {
						self?.popupViewController?.update(state: .listening(partialText: partialText))
					}
				},
				onFinalResult: { [weak self] result in
					// When recording is done, process the final text
					DispatchQueue.main.async {
						self?.processFinalTranscription(result)
					}
				},
				errorOccurred: { [weak self] _ in
					self?.popupViewController?.close()
				}
			)
		} catch {
			self.popupViewController?.close()
		}
	}

	private func handleKeyUp() {
		guard self.isRecording else { return }
		self.isRecording = false

		self.dictationManager.stopAndFinalize()
	}

	private func processFinalTranscription(_ transcribedText: String) {
		guard !transcribedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
			self.popupViewController?.close()
			return
		}

		self.fetchClaudeResponse(for: transcribedText)
	}

	private func fetchClaudeResponse(for prompt: String) {
		self.popupViewController?.update(state: .processing)

		self.claudeManager.sendPrompt(prompt: prompt) { result in
			DispatchQueue.main.async {
				switch result {
				case let .success(APIResponse):
					var response: String!

					for result in APIResponse.content {
						if result.type == "text" {
							response = result.text
						}
					}

					var instructions: [String] = []

					for match in self.textRegex.matches(in: response, options: [], range: NSRange(location: 0, length: response.utf16.count)) {
						var bullets: String

						if let r = Range(match.range(at: 1), in: response) {
							bullets = String(response[r])

							for match in self.bulletRegex.matches(in: bullets, range: NSRange(location: 0, length: bullets.utf16.count)) {
								if let r = Range(match.range(at: 1), in: bullets) {
									instructions.append(String(bullets[r]))
								}
							}
						}
					}

					if instructions.isEmpty {
						guard let popupViewController = self.popupViewController else { return }

						let displayText = response.replacingOccurrences(of: "```\n", with: "")
						popupViewController.update(state: .showingResult(fullText: displayText))

						self.speechManager.textToSpeech(text: displayText) { result in
							switch result {
							case let .success(data):
								self.speechManager.playAudio(data: data) {
									popupViewController.revealContinueHint()

									popupViewController.dismissTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: false) { _ in
										popupViewController.close()
									}
								}

							case let .failure(error):
								print("Error Occurred: \(error.localizedDescription)")
								self.popupViewController?.update(state: .showingResult(fullText: "Speech error occurred."))
							}
						}
					} else {
						self.progressThroughInstructions(instructions: instructions, aggregateString: "", index: 0)
					}
				case let .failure(error):
					print("Error Occurred: \(error.localizedDescription)")
					self.popupViewController?.update(state: .showingResult(fullText: "Claude API failed to respond."))
				}
			}
		}
	}

	private func progressThroughInstructions(instructions: [String], aggregateString: String, index: Int) {
		guard let popupViewController = self.popupViewController else { return }
		var aggregateString = aggregateString

		if index == instructions.count {
			popupViewController.update(state: .showingResult(fullText: "\(aggregateString)\nWould you like me to show you how to do this?"))

			self.speechManager.textToSpeech(text: "Would you like me to show you how to do this?") { result in
				switch result {
				case let .success(data):
					self.speechManager.playAudio(data: data) {
						popupViewController.revealShowMeButton {
							popupViewController.hideContinueHint()
							popupViewController.update(st
[truncated — 6244 more characters]
```

### PopupViewController.swift

```swift
//
//  PopupViewController.swift
//  VoiceApp
//
//  Created by Zahtec on 10/24/25.
//

import Cocoa
import QuartzCore

class PopupViewController {
	enum PopupState {
		case listening(partialText: String)
		case processing
		case showingResult(fullText: String)
	}

	private var window: NSPanel?
	private var visualEffectView: NSVisualEffectView!
	private var statusLabel: NSTextField!
	private var mainTextField: NSTextField!
	private var closeButton: NSButton!
	private var scrollView: NSScrollView!
	private var closeBackgroundView: NSView!

	private var awaitingFirstStream = true
	private var isTranscribing = false

	private let initialWindowWidth: CGFloat = 320
	private let initialWindowHeight: CGFloat = 56
	private let maxWindowHeight: CGFloat = 480
	private let contentInsets = NSEdgeInsets(top: 12, left: 16, bottom: 10, right: 16)
	private let headerSpacing: CGFloat = 12 // spacing between status label and text area

	private let hintTopSpacing: CGFloat = 8
	private var continueHintContainer: NSVisualEffectView!
	private var continueHintStack: NSStackView!
	private var continueHintCollapsedConstraint: NSLayoutConstraint!

	private var bottomAccessoryStack: NSStackView!
	private var showMeContainer: NSVisualEffectView!
	private var showMeButton: NSButton!
	private var showMeCollapsedConstraint: NSLayoutConstraint!

	private var handleShowMeTapped: (() -> Void)?

	private let fadeDuration: TimeInterval = 0.20
	var dismissTimer: Timer?

	func show() {
		self.runOnMain {
			self.setupWindow()
			self.setupUI()
			self.awaitingFirstStream = true
			self.isTranscribing = true
			self.update(state: .listening(partialText: "…"))

			self.fadeInWindow()
		}
	}

	func update(state: PopupState) {
		self.runOnMain {
			guard let _ = self.window else { return }

			self.dismissTimer?.invalidate()

			switch state {
			case let .listening(partialText):
				self.statusLabel.stringValue = "Listening…"
				self.startPulse()

				if self.isFirstRealStreamText(partialText) {
					self.awaitingFirstStream = false
					self.showTranscriptionArea(with: partialText)
					self.resizeToFitContent()
				} else if !self.awaitingFirstStream {
					self.mainTextField.stringValue = partialText
					self.resizeToFitContent()
				}

			case .processing:
				self.startPulse()
				self.statusLabel.stringValue = "Thinking…"

				if !self.awaitingFirstStream {
					self.resizeToFitContent()
				}

			case let .showingResult(fullText):
				self.stopPulse()
				self.scrollView.isHidden = false

				self.statusLabel.stringValue = "Clearn Help"
				self.showTranscriptionArea(with: fullText)
				self.mainTextField.stringValue = fullText

				self.dismissTimer?.invalidate()
				self.resizeToFitContent()
			}
		}
	}

	func close() {
		self.runOnMain {
			guard let _ = self.window else {
				self.isTranscribing = false
				return
			}

			self.stopPulse()
			self.dismissTimer?.invalidate()

			self.fadeOutAndOrderOut { [weak self] in
				guard let self = self else { return }
				self.window = nil
				self.isTranscribing = false
				self.visualEffectView = nil
				self.statusLabel = nil
				self.mainTextField = nil
				self.scrollView = nil

				self.continueHintContainer = nil
				self.showMeContainer = nil
				self.bottomAccessoryStack = nil
			}
		}
	}

	// MARK: Public revealers for bottom accessories

	func revealContinueHint() {
		self.runOnMain {
			guard let _ = self.window else { return }
			// Show hint, hide button
			self.bottomAccessoryStack.isHidden = false

			self.showMeCollapsedConstraint.isActive = true
			self.showMeContainer.isHidden = true

			self.continueHintCollapsedConstraint.isActive = false
			self.continueHintContainer.isHidden = false

			self.resizeToFitContent()
		}
	}

	func hideContinueHint() {
		self.runOnMain {
			guard let _ = self.window else { return }
			// Show hint, hide button
			self.bottomAccessoryStack.isHidden = true

			self.showMeCollapsedConstraint.isActive = true
			self.showMeContainer.isHidden = true

			self.continueHintCollapsedConstraint.isActive = true
			self.continueHintContainer.isHidden = true

			self.resizeToFitContent()
		}
	}

	func revealShowMeButton(action: @escaping () -> Void) {
		self.handleShowMeTapped = action

		self.runOnMain {
			guard let _ = self.window else { return }
			// Show button, hide hint
			self.bottomAccessoryStack.isHidden = false

			self.continueHintCollapsedConstraint.isActive = true
			self.continueHintContainer.isHidden = true

			self.showMeCollapsedConstraint.isActive = false
			self.showMeContainer.isHidden = false

			self.resizeToFitContent()
		}
	}

	// MARK: Setup

	private func setupWindow() {
		let windowSize = NSSize(width: initialWindowWidth, height: initialWindowHeight)
		guard let screenFrame = NSScreen.main?.visibleFrame else { return }
		let xPos = screenFrame.maxX - windowSize.width - 10
		let yPos = screenFrame.maxY - windowSize.height - 10
		let windowRect = NSRect(x: xPos, y: yPos, width: windowSize.width, height: windowSize.height)

		let panel = NSPanel(contentRect: windowRect,
		                    styleMask: [.borderless, .nonactivatingPanel],
		                    backing: .buffered,
		                    defer: false)
		panel.isFloatingPanel = true
		panel.level = .floating
		panel.backgroundColor = .clear
		panel.isOpaque = false
		panel.hidesOnDeactivate = false
		panel.collectionBehavior = [.canJoinAllSpaces, .transient]

		panel.alphaValue = 0.0

		self.window = panel
		self.window?.orderFront(nil)
	}

	private func setupUI() {
		guard let contentView = window?.contentView else { return }

		self.visualEffectView = NSVisualEffectView(frame: contentView.bounds)
		self.visualEffectView.material = .hudWindow
		self.visualEffectView.blendingMode = .withinWindow
		self.visualEffectView.state = .active
		self.visualEffectView.autoresizingMask = [.width, .height]
		self.visualEffectView.wantsLayer = true
		self.visualEffectView.layer?.cornerRadius = 16.0
		contentView.addSubview(self.visualEffectView)

		self.statusLa
[truncated — 14154 more characters]
```