# Project export: Snappy

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: UC Berkeley AI Hackathon 2026
- Tagline: Every moment, one snap away.
- Devpost: https://devpost.com/software/snappy-mov1nq
- GitHub: https://github.com/abcdemilyfgh/ai-hackathon
- Video: https://www.youtube.com/embed/DeGyypiqNXk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Emily Hua (6 commits)

## Devpost submission (written by the team)

### Inspiration

Both of us have been vlogging our lives for a while, whether on trips or during exciting days and weeks, and we genuinely love the process. But it's so tedious. We record a ton of clips and then have to sort through all of them, and that inconvenience makes us way more likely to put it off. We'd end up not finishing a video we took a bunch of clips for until a year later... or never going through them at all. So when we decided to work together on this hackathon, our first thought was: let's solve a problem we both actively face. Something we know would make life easier for content creators like us.

### What it does

Snappy is a macOS app that watches and listens to your video clips so you can find the right moment by just asking for it. You point it at a folder and it automatically: Transcribes every clip Tags and summarizes each one so you know what's inside at a glance Flags filler words and repeated phrases so you can tighten your delivery Lets you chat with your whole library, "find the clip where I talk about ___", and it takes you right there Searches and filters by tag, keyword, or flagged-only, sorted by name, length, or number of issues No renaming files, no scrubbing through footage one clip at a time. You just open a folder and talk to your footage. Sort through your clips in a snap.

### How we built it

Snappy is a native macOS SwiftUI app. When you pick a folder, it loads your clips in place (nothing gets copied or moved) and kicks off an indexing pipeline for each clip: Audio extraction, we use ffmpeg to pull the audio track out of each video. Transcription, that audio goes to Deepgram (Nova-2), which gives us an accurate transcript. Local analysis, we scan the transcript ourselves for filler words and repeated phrases, then flag clips that have issues. Understanding, we send the transcript to Claude (Sonnet 4) to generate tags and a short summary for each clip. Chat, the chat feature feeds Claude context across all your clips, so it can reason about your whole library and point you to the exact moment you're describing. To keep our API keys safe, all requests route through a Cloudflare Worker that holds the keys server-side, so nothing sensitive ships in the app itself. We also cache every result locally as JSON with a stable hash, so reopening a folder is instant instead of re-indexing everything. On the frontend, we built a searchable clip list with tag chips and ⚠️ badges, a detail view with an inline-highlighted transcript (orange for repeated phrases, yellow for filler words), and a chat interface with suggested prompts so it's obvious what you can do.

### Challenges we ran into

The biggest one at the start was just figuring out where to begin and which direction to go in. We started from an existing screen-watching app to reuse some of the API infrastructure, which meant a lot of stripping out and repurposing code that wasn't built for what we wanted. Other bumps along the way: Wiring up the transcription pipeline end to end, getting ffmpeg, Deepgram, and Claude to hand off to each other cleanly. Caching that actually survives relaunches, so the demo loads instantly instead of re-indexing every clip live. The app icon (of all things!) with broken asset sets and a grey placeholder icon. General SwiftUI state and rename headaches as we went from the original project name to Snappy.

### Accomplishments we're proud of

Our first idea together, brought to life, and honestly the start of an exciting time for both of us to keep creating together. It was so satisfying seeing the app with the logo we picked sitting in my dock, and the window opening for the first time when I ran the code. After grinding on the backend, watching the frontend pull everything together was really cool, it suddenly felt like a real app instead of a pile of functions. And the fact that it actually does the thing we set out to do: you drop in a messy folder of clips and it just... understands them.

### What we learned

Honestly, all of it. We learned how to: Build a real native macOS app in SwiftUI from the ground up Chain multiple AI services (Deepgram + Claude) into one smooth pipeline Keep API keys safe with a Cloudflare Worker proxy Design caching so the experience feels instant And maybe most importantly, how to scope an idea down to something we could actually ship in a hackathon, and divide and conquer between the two of us.

### What's next

A video display view that flags sections directly in the timeline at the exact timestamp, so you can see and jump to the rough spots visually. Integration with editing software, iMovie, Premiere Pro, Final Cut, CapCut, so the AI can help you directly cut and trim sections, expediting the whole editing process instead of just pointing you at the right clip. The dream is that Snappy doesn't just find your moments, it helps you edit them too.

## README (from the GitHub repository)

# Snappy 🔴

**Every moment, one snap away.** · *Talk to your footage.*

Snappy is a macOS app that watches and listens to your video clips so you can find the right moment by *just asking for it*. Point it at a folder and it transcribes, tags, summarizes, and flags every clip automatically, then lets you chat with your whole library like it's a person who watched all of it.

![snappy](snappy-hero-banner.png)

---

## The problem

Anyone who vlogs ends up with folders full of unnamed clips like `IMG_4471.mov` or `final_v3_REAL.mp4`, with no idea what's inside without opening each one. Finding "the take where I nailed the intro" means scrubbing through dozens of files by hand. It's so tedious it makes you put off editing entirely (sometimes for a year, sometimes forever).

## What it does

Open a folder and snappy instantly:

- 🎙️ **Transcribes** every clip (Deepgram Nova-2)
- 🏷️ **Tags & summarizes** each one (Claude Sonnet 4)
- ⚠️ **Flags** filler words and repeated phrases so you can tighten your delivery
- 💬 **Chat** across your whole library, like *"Find the clip where I talk about pricing"*, and snappy takes you right there
- 🔍 **Search & filter** by tag, keyword, or flagged-only, and sort by name, length, or number of issues

No uploading, no renaming files, no manual logging. Just open a folder and talk to your footage.

## How it works

1. **Pick a folder.** Clips load in place (nothing copied or moved).
2. **Auto-index** per clip: extract audio (`ffmpeg`), transcribe (Deepgram), analyze fillers/repeats locally, then tag and summarize (Claude).
3. **Browse** a searchable list with tag chips, summaries, and ⚠️ badges.
4. **Dig in.** Open any clip for an inline-highlighted transcript (orange for repeated phrases, yellow for filler words).
5. **Ask.** Chat reasons across every clip to find exactly what you mean.

Results are cached locally with a stable hash, so reopening a folder is instant.

## Built with

**Languages:** Swift, TypeScript/JavaScript
**Framework / UI:** SwiftUI
**Platform:** macOS (built in Xcode)
**AI / APIs:** Anthropic Claude (Sonnet 4), Deepgram (Nova-2)
**Cloud:** Cloudflare Workers (API-key proxy)
**Tools:** ffmpeg
**Storage:** local JSON cache (stable-hash, on-device)

## Setup

```bash
# Requires ffmpeg
brew install ffmpeg

# Open in Xcode, then ⌘R
open snappy.xcodeproj
```

API requests route through a Cloudflare Worker that holds the `ANTHROPIC_API_KEY` and `DEEPGRAM_API_KEY`, so no keys ship in the app.

## Privacy & responsible AI

Privacy was our top priority since snappy handles personal footage:

- **Your clips never leave your machine.** snappy reads them in place and caches results **locally** on your device.
- Only the **audio transcript** is sent to our AI services. The video files themselves are never uploaded.
- **API keys stay server-side** in a Cloudflare Worker, so nothing sensitive ships in the app.
- **Efficient by design:** each clip is only processed once (cached), and filler/repeat detection runs locally to avoid unnecessary inference.
- snappy **assists creators, it doesn't replace them.** You stay in control, and since transcription accuracy varies across accents and audio, AI output is a verifiable starting point with the original clip always one click away.

## What's next

- A **video timeline view** that flags rough sections directly at their timestamps.
- **Integration with editing software** (iMovie, Premiere Pro, Final Cut, CapCut) so the AI can help cut and trim sections directly. Not just *finding* your moments, but helping you *edit* them.

## Links

- **Code:** https://github.com/abcdemilyfgh/ai-hackathon

---

*Built with ❤️ by two sisters who got tired of never finishing their vlogs.*



## Detected evidence (automated analysis)

Indexed codebase: 41 recognized source files, 374 KB.
- Swift (language) — detected in the code
- TypeScript (language) — detected in the code
- Anthropic (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (69 of 69)

```
.gitignore
AGENTS.md
appcast.xml
CLAUDE.md
leanring-buddy.xcodeproj/project.pbxproj
leanring-buddy.xcodeproj/project.xcworkspace/contents.xcworkspacedata
leanring-buddy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
leanring-buddy.xcodeproj/xcshareddata/xcschemes/leanring-buddy.xcscheme
leanring-buddy.xcodeproj/xcuserdata/emilyhua.xcuserdatad/xcschemes/xcschememanagement.plist
leanring-buddy.xcodeproj/xcuserdata/julianalvarez.xcuserdatad/xcschemes/xcschememanagement.plist
leanring-buddy.xcodeproj/xcuserdata/thorfinn.xcuserdatad/xcschemes/xcschememanagement.plist
leanring-buddy/AGENTS.md
leanring-buddy/AppBundleConfiguration.swift
leanring-buddy/AppleSpeechTranscriptionProvider.swift
leanring-buddy/AssemblyAIStreamingTranscriptionProvider.swift
leanring-buddy/Assets.xcassets/AccentColor.colorset/Contents.json
leanring-buddy/Assets.xcassets/add-a-project-in-codex-from-sidebar.imageset/Contents.json
leanring-buddy/Assets.xcassets/add-a-project-in-codex.imageset/Contents.json
leanring-buddy/Assets.xcassets/AppIcon.appiconset/Contents.json
leanring-buddy/Assets.xcassets/codex-app-screenshot.imageset/Contents.json
leanring-buddy/Assets.xcassets/codex-home-screen.imageset/Contents.json
leanring-buddy/Assets.xcassets/codex-permissions.imageset/Contents.json
leanring-buddy/Assets.xcassets/Contents.json
leanring-buddy/Assets.xcassets/discord-logo.imageset/Contents.json
leanring-buddy/Assets.xcassets/git-tools-prompt.imageset/Contents.json
leanring-buddy/Assets.xcassets/google-logo.imageset/Contents.json
leanring-buddy/Assets.xcassets/inside-the-makesomething-project-folder.imageset/Contents.json
leanring-buddy/Assets.xcassets/makesomething-project-folder-in-downloads.imageset/Contents.json
leanring-buddy/Assets.xcassets/steve.imageset/Contents.json
leanring-buddy/BuddyAudioConversionSupport.swift
leanring-buddy/BuddyDictationManager.swift
leanring-buddy/BuddyTranscriptionProvider.swift
leanring-buddy/ChatView.swift
leanring-buddy/ClaudeAPI.swift
leanring-buddy/ClickyAnalytics.swift
leanring-buddy/ClipCache.swift
leanring-buddy/ClipDetailView.swift
leanring-buddy/ClipModels.swift
leanring-buddy/ClipStore.swift
leanring-buddy/CompanionManager.swift
leanring-buddy/CompanionPanelView.swift
leanring-buddy/CompanionResponseOverlay.swift
leanring-buddy/CompanionScreenCaptureUtility.swift
leanring-buddy/DesignSystem.swift
leanring-buddy/ElementLocationDetector.swift
leanring-buddy/ElevenLabsTTSClient.swift
leanring-buddy/FileBrowserView.swift
leanring-buddy/GlobalPushToTalkShortcutMonitor.swift
leanring-buddy/IndexingService.swift
leanring-buddy/Info.plist
leanring-buddy/leanring_buddyApp.swift
leanring-buddy/leanring-buddy.entitlements
leanring-buddy/MainView.swift
leanring-buddy/MenuBarPanelManager.swift
leanring-buddy/OpenAIAPI.swift
leanring-buddy/OpenAIAudioTranscriptionProvider.swift
leanring-buddy/OverlayWindow.swift
leanring-buddy/WindowPositionManager.swift
leanring-buddyTests/leanring_buddyTests.swift
leanring-buddyUITests/leanring_buddyUITests.swift
leanring-buddyUITests/leanring_buddyUITestsLaunchTests.swift
LICENSE
README.md
scripts/README.md
scripts/release.sh
worker/.gitignore
worker/package.json
worker/src/index.ts
worker/wrangler.toml
```

### Dependencies

- worker/package.json: wrangler@^3.114.17

### Recent commits (newest first)

- Update tagline and correct capitalization in README
- Add snappy README with banner, built-with, and privacy section
- Rename to snappy, new app icon, flagged-only filter/sort
- Add empty state, indexing progress bar, chat suggestion chips
- Add README for chris
- Ignore wrangler cache
- chris: clip indexer (Deepgram + Claude) with chat, flags, caching
- Update README.md
- Remove hardcoded Anthropic API key from build settings
- Update README.md
- Update README.md
- Update README.md
- Add MIT license
- Merge branch 'main' of https://github.com/farzaa/clicky
- Replace hero image with demo gif
- Update README.md
- Single plain-text Claude Code prompt that clones + sets up everything
- Make Claude Code prompt a direct paste into an existing session
- Make Claude Code setup prompt more conversational
- Merge branch 'main' of https://github.com/farzaa/clicky

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

### CLAUDE.md

```markdown
AGENTS.md
```

### AGENTS.md

```markdown
# Clicky - Agent Instructions

<!-- This is the single source of truth for all AI coding agents. CLAUDE.md is a symlink to this file. -->
<!-- AGENTS.md spec: https://github.com/agentsmd/agents.md — supported by Claude Code, Cursor, Copilot, Gemini CLI, and others. -->

## Overview

macOS menu bar companion app. Lives entirely in the macOS status bar (no dock icon, no main window). Clicking the menu bar icon opens a custom floating panel with companion voice controls. Uses push-to-talk (ctrl+option) to capture voice input, transcribes it via AssemblyAI streaming, and sends the transcript + a screenshot of the user's screen to Claude. Claude responds with text (streamed via SSE) and voice (ElevenLabs TTS). A blue cursor overlay can fly to and point at UI elements Claude references on any connected monitor.

All API keys live on a Cloudflare Worker proxy — nothing sensitive ships in the app.

## Architecture

- **App Type**: Menu bar-only (`LSUIElement=true`), no dock icon or main window
- **Framework**: SwiftUI (macOS native) with AppKit bridging for menu bar panel and cursor overlay
- **Pattern**: MVVM with `@StateObject` / `@Published` state management
- **AI Chat**: Claude (Sonnet 4.6 default, Opus 4.6 optional) via Cloudflare Worker proxy with SSE streaming
- **Speech-to-Text**: AssemblyAI real-time streaming (`u3-rt-pro` model) via websocket, with OpenAI and Apple Speech as fallbacks
- **Text-to-Speech**: ElevenLabs (`eleven_flash_v2_5` model) via Cloudflare Worker proxy
- **Screen Capture**: ScreenCaptureKit (macOS 14.2+), multi-monitor support
- **Voice Input**: Push-to-talk via `AVAudioEngine` + pluggable transcription-provider layer. System-wide keyboard shortcut via listen-only CGEvent tap.
- **Element Pointing**: Claude embeds `[POINT:x,y:label:screenN]` tags in responses. The overlay parses these, maps coordinates to the correct monitor, and animates the blue cursor along a bezier arc to the target.
- **Concurrency**: `@MainActor` isolation, async/await throughout
- **Analytics**: PostHog via `ClickyAnalytics.swift`

### API Proxy (Cloudflare Worker)

The app never calls external APIs directly. All requests go through a Cloudflare Worker (`worker/src/index.ts`) that holds the real API keys as secrets.

| Route | Upstream | Purpose |
|-------|----------|---------|
| `POST /chat` | `api.anthropic.com/v1/messages` | Claude vision + streaming chat |
| `POST /tts` | `api.elevenlabs.io/v1/text-to-speech/{voiceId}` | ElevenLabs TTS audio |
| `POST /transcribe-token` | `streaming.assemblyai.com/v3/token` | Fetches a short-lived (480s) AssemblyAI websocket token |

Worker secrets: `ANTHROPIC_API_KEY`, `ASSEMBLYAI_API_KEY`, `ELEVENLABS_API_KEY`
Worker vars: `ELEVENLABS_VOICE_ID`

### Key Architecture Decisions

**Menu Bar Panel Pattern**: The companion panel uses `NSStatusItem` for the menu bar icon and a custom borderless `NSPanel` for the floating control panel. This gives full control over appearance (dark, rounded corners, custom shadow) and
[truncated — 10015 more characters]
```

### worker/package.json

```
{
  "name": "clicky-proxy",
  "private": true,
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy"
  },
  "devDependencies": {
    "wrangler": "^3.114.17"
  }
}

```

### worker/src/index.ts

```typescript
/**
 * Clicky Proxy Worker
 *
 * Proxies requests to Claude, ElevenLabs, AssemblyAI, and Deepgram APIs
 * so the app never ships with raw API keys. Keys are stored as Cloudflare secrets.
 *
 * Routes:
 *   POST /chat              → Anthropic Messages API (streaming)
 *   POST /tts               → ElevenLabs TTS API
 *   POST /transcribe-token  → AssemblyAI streaming token
 *   POST /transcribe        → Deepgram prerecorded transcription
 */

interface Env {
  ANTHROPIC_API_KEY: string;
  ELEVENLABS_API_KEY: string;
  ELEVENLABS_VOICE_ID: string;
  ASSEMBLYAI_API_KEY: string;
  DEEPGRAM_API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    try {
      if (url.pathname === "/chat") {
        return await handleChat(request, env);
      }

      if (url.pathname === "/tts") {
        return await handleTTS(request, env);
      }

      if (url.pathname === "/transcribe-token") {
        return await handleTranscribeToken(env);
      }

      if (url.pathname === "/transcribe") {
        return await handleTranscribe(request, env);
      }
    } catch (error) {
      console.error(`[${url.pathname}] Unhandled error:`, error);
      return new Response(
        JSON.stringify({ error: String(error) }),
        { status: 500, headers: { "content-type": "application/json" } }
      );
    }

    return new Response("Not found", { status: 404 });
  },
};

async function handleChat(request: Request, env: Env): Promise<Response> {
  const body = await request.text();

  const response = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body,
  });

  if (!response.ok) {
    const errorBody = await response.text();
    console.error(`[/chat] Anthropic API error ${response.status}: ${errorBody}`);
    return new Response(errorBody, {
      status: response.status,
      headers: { "content-type": "application/json" },
    });
  }

  return new Response(response.body, {
    status: response.status,
    headers: {
      "content-type": response.headers.get("content-type") || "text/event-stream",
      "cache-control": "no-cache",
    },
  });
}

async function handleTranscribeToken(env: Env): Promise<Response> {
  const response = await fetch(
    "https://streaming.assemblyai.com/v3/token?expires_in_seconds=480",
    {
      method: "GET",
      headers: {
        authorization: env.ASSEMBLYAI_API_KEY,
      },
    }
  );

  if (!response.ok) {
    const errorBody = await response.text();
    console.error(`[/transcribe-token] AssemblyAI token error ${response.status}: ${errorBody}`);
    return new Response(errorBody, {
      status: response.status,
      headers: { "content-type": "application/json" },
    });
  }

  const data = await response.text();
  return new Response(data, {
    status: 200,
    headers: { "content-type": "application/json" },
  });
}

async function handleTranscribe(request: Request, env: Env): Promise<Response> {
  // The app sends the raw audio/video bytes as the request body.
  // Query params (model, smart_format, etc.) are passed straight through to Deepgram.
  const incoming = new URL(request.url);
  const dgUrl = new URL("https://api.deepgram.com/v1/listen");
  dgUrl.search = incoming.search; // forward ?model=...&smart_format=true etc.

  const response = await fetch(dgUrl.toString(), {
    method: "POST",
    headers: {
      authorization: `Token ${env.DEEPGRAM_API_KEY}`,
      "content-type": request.headers.get("content-type") || "application/octet-stream",
    },
    body: request.body,
  });

  if (!response.ok) {
    const errorBody = await response.text();
    console.error(`[/transcribe] Deepgram error ${response.status}: ${errorBody}`);
    return new Response(errorBody, {
      status: response.status,
      headers: { "content-type": "application/json" },
    });
  }

  return new Response(response.body, {
    status: 200,
    headers: { "content-type": "application/json" },
  });
}

async function handleTTS(request: Request, env: Env): Promise<Response> {
  const body = await request.text();
  const voiceId = env.ELEVENLABS_VOICE_ID;

  const response = await fetch(
    `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
    {
      method: "POST",
      headers: {
        "xi-api-key": env.ELEVENLABS_API_KEY,
        "content-type": "application/json",
        accept: "audio/mpeg",
      },
      body,
    }
  );

  if (!response.ok) {
    const errorBody = await response.text();
    console.error(`[/tts] ElevenLabs API error ${response.status}: ${errorBody}`);
    return new Response(errorBody, {
      status: response.status,
      headers: { "content-type": "application/json" },
    });
  }

  return new Response(response.body, {
    status: response.status,
    headers: {
      "content-type": response.headers.get("content-type") || "audio/mpeg",
    },
  });
}

```

### appcast.xml

```xml
<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
    <channel>
        <title>makesomething</title>
        <item>
            <title>2.0</title>
            <pubDate>Sat, 14 Mar 2026 09:56:02 -0700</pubDate>
            <sparkle:version>11</sparkle:version>
            <sparkle:shortVersionString>2.0</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/julianjear/makesomething-mac-app/releases/download/v2.0/makesomething.dmg" length="11852050" type="application/octet-stream" sparkle:edSignature="Y9o+YBln/BXAwYeJLuGV0E7B9q9/+kuhhFlNRyekZuYLPwbTYoL7aUYhurAQFfmiSuWuCUNHKXgtzaKCyaMGCQ=="/>
        </item>
        <item>
            <title>1.9</title>
            <pubDate>Sat, 14 Mar 2026 08:33:19 -0700</pubDate>
            <sparkle:version>10</sparkle:version>
            <sparkle:shortVersionString>1.9</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/julianjear/makesomething-mac-app/releases/download/v1.9/makesomething.dmg" length="11851619" type="application/octet-stream" sparkle:edSignature="l0OE/1XAsVgbyyP0vty+50oqzkTeR1yTRsuKtCU/m8UHduT/9cafKI/x1Vy5p8RW2t5JKDFIqmar/jRf6NRuDg=="/>
        </item>
        <item>
            <title>1.8</title>
            <pubDate>Wed, 11 Mar 2026 10:17:12 -0700</pubDate>
            <sparkle:version>9</sparkle:version>
            <sparkle:shortVersionString>1.8</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/julianjear/makesomething-mac-app/releases/download/v1.8/makesomething.dmg" length="11489099" type="application/octet-stream" sparkle:edSignature="K7n75BUbzhzTbrnYDQDcsmMQGb3bEjvl5yxSIdBGtbanNWrEUym2sozah5hGkAQ0sLShrPhx8FDBunmd2T3rAw=="/>
        </item>
    </channel>
</rss>
```

### leanring-buddy/MainView.swift

```swift
import SwiftUI

struct MainView: View {
    @StateObject private var store = ClipStore()

    var body: some View {
        HSplitView {
            FileBrowserView(store: store)
            ChatView(store: store)
        }
        .frame(minWidth: 760, minHeight: 520)
    }
}


```

### leanring-buddyUITests/leanring_buddyUITestsLaunchTests.swift

```swift
//
//  leanring_buddyUITestsLaunchTests.swift
//  leanring-buddyUITests
//
//  Created by thorfinn on 3/2/26.
//

import XCTest

final class leanring_buddyUITestsLaunchTests: XCTestCase {

    override class var runsForEachTargetApplicationUIConfiguration: Bool {
        true
    }

    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    @MainActor
    func testLaunch() throws {
        let app = XCUIApplication()
        app.launch()

        // Insert steps here to perform after app launch but before taking a screenshot,
        // such as logging into a test account or navigating somewhere in the app

        let attachment = XCTAttachment(screenshot: app.screenshot())
        attachment.name = "Launch Screen"
        attachment.lifetime = .keepAlways
        add(attachment)
    }
}

```

### leanring-buddy/leanring_buddyApp.swift

```swift
//
//  leanring_buddyApp.swift
//  chris
//

import SwiftUI

@main
struct leanring_buddyApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @Environment(\.openWindow) private var openWindow

    var body: some Scene {
        Window("Snappy", id: "main") {
            MainView()
        }

        MenuBarExtra("Snappy", systemImage: "film.stack") {
            Button("Open Snappy") {
                NSApp.activate(ignoringOtherApps: true)
                openWindow(id: "main")
            }
            Divider()
            Button("Quit Snappy") { NSApp.terminate(nil) }
        }
    }
}

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        NSApp.setActivationPolicy(.regular)
        NSApp.activate(ignoringOtherApps: true)
    }
}


```

### leanring-buddy/AppBundleConfiguration.swift

```swift
//
//  AppBundleConfiguration.swift
//  leanring-buddy
//
//  Shared helper for reading runtime configuration from the built app bundle.
//

import Foundation

enum AppBundleConfiguration {
    static func stringValue(forKey key: String) -> String? {
        if let value = Bundle.main.object(forInfoDictionaryKey: key) as? String {
            let trimmedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
            if !trimmedValue.isEmpty {
                return trimmedValue
            }
        }

        guard let resourceInfoPath = Bundle.main.path(forResource: "Info", ofType: "plist"),
              let resourceInfo = NSDictionary(contentsOfFile: resourceInfoPath),
              let value = resourceInfo[key] as? String else {
            return nil
        }

        let trimmedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmedValue.isEmpty ? nil : trimmedValue
    }
}

```

### leanring-buddyTests/leanring_buddyTests.swift

```swift
//
//  leanring_buddyTests.swift
//  leanring-buddyTests
//
//  Created by thorfinn on 3/2/26.
//

import Testing
@testable import leanring_buddy

struct leanring_buddyTests {

    @Test func firstPermissionRequestUsesSystemPromptOnly() async throws {
        let presentationDestination = WindowPositionManager.permissionRequestPresentationDestination(
            hasPermissionNow: false,
            hasAttemptedSystemPrompt: false
        )

        #expect(presentationDestination == .systemPrompt)
    }

    @Test func repeatedPermissionRequestOpensSystemSettings() async throws {
        let presentationDestination = WindowPositionManager.permissionRequestPresentationDestination(
            hasPermissionNow: false,
            hasAttemptedSystemPrompt: true
        )

        #expect(presentationDestination == .systemSettings)
    }

    @Test func knownGrantedScreenRecordingPermissionSkipsTheGate() async throws {
        let shouldTreatPermissionAsGranted = WindowPositionManager.shouldTreatScreenRecordingPermissionAsGrantedForSessionLaunch(
            hasScreenRecordingPermissionNow: false,
            hasPreviouslyConfirmedScreenRecordingPermission: true
        )

        #expect(shouldTreatPermissionAsGranted)
    }

}

```

[29 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]