# Project export: Context

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: OpenAI Build Week
- Tagline: Context is an AI teammate you text to understand and manage everything happening across your work.
- Devpost: https://devpost.com/software/falafel
- GitHub: https://github.com/falafell99/context
- Video: https://www.youtube.com/embed/xDIVPZN3uYk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Rafael (3 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# Context

**You do not search your workspace anymore. You text it.**

Context is a native iOS AI teammate that lives in a messaging interface. It connects to your work tools, understands what changed, and replies like a concise coworker — not like a dashboard.

## Why

Modern work is scattered across GitHub, Notion, calendars, Slack, and tickets. The common workflow is still manual: open five apps, scan updates, decide what matters, then act.

Context flips that interaction. You ask:

> What should I work on first?

Context reads your workspace, reasons across sources, and responds with a prioritized answer plus tappable source cards.

## What Works

- **Native SwiftUI chat UI** inspired by Apple Messages.
- **FastAPI backend** with streaming-compatible responses.
- **Groq-powered LLM** for fast development responses.
- **Real GitHub reads** for repo metadata, PRs, issues, commits, reviews, and checks.
- **Notion page support** for launch notes and project context, with demo fallback when unconfigured.
- **Real Calendar ICS reads** for today's events and meetings.
- **Slack connector** for team updates, with demo fallback when unconfigured.
- **Workspace setup sheet** for demo configuration without editing app code.
- **Daily brief endpoint** that summarizes GitHub, Notion, Calendar, and Slack context without a user prompt.
- **Local conversation history** so the chat survives app relaunches.
- **Thinking states** like "Searching GitHub," "Reading Notion," "Checking Calendar."
- **Tappable source cards** with native iOS preview sheets.
- **Safe action drafts** for GitHub issues, PR comments, and calendar events.
- **Confirmed GitHub issue creation and PR comment posting** through the backend.
- **New Chat button** to start fresh conversations.

## Demo Flow

1. Open the app.
2. Context sends a morning brief.
3. Ask: `What should I work on first?`
4. Context checks GitHub, Notion, and Calendar.
5. Context recommends the highest-priority work with sources.
6. Tap a source card to inspect evidence.
7. Tap `Draft issue` to prepare an action.
8. Confirm creation only when ready.

## Architecture

```
iOS SwiftUI app
  ├─ ChatView / ChatViewModel
  ├─ Source cards and action preview sheets
  └─ APIClient

FastAPI backend
  ├─ /chat — streaming-compatible response stream
  ├─ /brief — daily workspace brief
  ├─ /connections — configured/demo source status
  ├─ /workspace/setup — runtime demo workspace configuration
  ├─ /actions/draft/github-issue
  ├─ /actions/draft/github-pr-comment
  ├─ /actions/draft/calendar-event
  ├─ /actions/create/github-issue
  ├─ /actions/create/github-pr-comment
  ├─ GitHub connector
  ├─ Calendar ICS connector
  ├─ Notion page connector
  └─ Groq LLM provider
```

## Tech Stack

- **iOS:** SwiftUI, async/await, MVVM
- **Backend:** Python, FastAPI, Pydantic
- **AI:** Groq for development LLM responses
- **Workspace APIs:** GitHub REST API, Notion API, iCal/ICS calendar feeds, Slack Web API
- **Testing:** pytest, httpx ASGI tests

## How Codex And GPT-5.6 Were Used

### Codex

Codex served as the primary engineering teammate throughout the hackathon build:

- Designed and iterated the SwiftUI chat architecture.
- Built the FastAPI backend and streaming-compatible chat contract.
- Implemented real GitHub, Notion, and Calendar connectors.
- Added a lightweight workspace setup flow for demo configuration.
- Created safe action endpoints for drafting and confirming GitHub issue creation and PR comments.
- Wrote backend tests for connectors, orchestration, and action flows.
- Debugged iOS-to-backend streaming and converted it into a stable demo flow.
- Shaped the product narrative, Devpost positioning, and demo story.

### GPT-5.6

GPT-5.6 served as the planning and product reasoning partner:

- Refined the "AI teammate you text" concept.
- Prioritized MVP scope under hackathon constraints.
- Reviewed UX copy so Context sounds like a coworker, not a chatbot.
- Identified the strongest demo path: brief → prioritization → evidence → action.

## Try The Chat Endpoint

The iOS demo currently points to the local FastAPI backend at `http://127.0.0.1:8000`.

```bash
curl -N -X POST http://127.0.0.1:8000/chat \
  -H 'Content-Type: application/json' \
  -d '{"message":"What should I work on first?"}'
```

## Try The Daily Brief

```bash
curl http://127.0.0.1:8000/brief
```

## Submission Materials

- `DEVPOST_SUBMISSION.md` — copy-ready Devpost draft.
- `DEMO.md` — recording script and judge demo flow.
- `SUBMISSION_CHECKLIST.md` — final pre-submit checklist.


## Detected evidence (automated analysis)

Indexed codebase: 44 recognized source files, 244 KB.
- FastAPI (technology) — detected in the code
- Python (language) — detected in the code
- Swift (language) — detected in the code

## Codebase structure (from repository index)

### Files (54 of 54)

```
.gitignore
backend/.env.example
backend/.gitignore
backend/app/__init__.py
backend/app/actions.py
backend/app/brief.py
backend/app/calendar.py
backend/app/config.py
backend/app/connections.py
backend/app/github.py
backend/app/llm.py
backend/app/main.py
backend/app/models.py
backend/app/notion.py
backend/app/orchestrator.py
backend/app/slack.py
backend/app/tools.py
backend/app/workspace.py
backend/PLAN.md
backend/pytest.ini
backend/README.md
backend/requirements.txt
backend/scripts/smoke.sh
backend/tests/test_actions.py
backend/tests/test_api.py
backend/tests/test_brief.py
backend/tests/test_calendar.py
backend/tests/test_connections.py
backend/tests/test_github.py
backend/tests/test_notion.py
backend/tests/test_orchestrator.py
backend/tests/test_tools.py
Context.xcodeproj/project.pbxproj
Context.xcodeproj/project.xcworkspace/contents.xcworkspacedata
Context/APIClient.swift
Context/Assets.xcassets/AccentColor.colorset/Contents.json
Context/Assets.xcassets/AppIcon.appiconset/Contents.json
Context/Assets.xcassets/Contents.json
Context/ChatView.swift
Context/ChatViewModel.swift
Context/ContentView.swift
Context/ContextApp.swift
Context/ContextPickerView.swift
Context/DesignSystem.swift
Context/HapticManager.swift
Context/MessageHistoryStore.swift
Context/Models.swift
Context/OnboardingView.swift
DEMO.md
DEVPOST_SUBMISSION.md
NEXT_STEPS.md
README.md
render.yaml
SUBMISSION_CHECKLIST.md
```

### Dependencies

- backend/requirements.txt: fastapi@==0.115.6, groq@==0.13.0, httpx@==0.28.1, pydantic@==2.10.4, pydantic-settings@==2.7.0, pytest@==8.3.4, pytest-asyncio@==0.25.2, python-dotenv@==1.0.1, uvicorn[standard]@==0.34.0

### Recent commits (newest first)

- Polish submission and workspace setup
- Build Context hackathon demo
- Initial Commit

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

### SUBMISSION_CHECKLIST.md

```markdown
# Context Submission Checklist

## Before Recording

- [ ] Start backend from `backend/`.
- [ ] Run `backend/scripts/smoke.sh` from the project root, or `./scripts/smoke.sh` from `backend/`.
- [ ] Run iOS app on iPhone 17 Pro simulator.
- [ ] Confirm first message loads from `/brief`.
- [ ] Confirm `What should I work on first?` returns real GitHub context.
- [ ] Tap at least one source card.
- [ ] Show one safe draft action.
- [ ] If demoing PR comments or issue creation, confirm the action only after checking the target repo.

## Required Demo Shots

- [ ] Onboarding / first message.
- [ ] Morning brief in chat.
- [ ] Thinking steps.
- [ ] Prioritized answer with source cards.
- [ ] Source detail sheet.
- [ ] Draft action sheet.
- [ ] Connect workspace sheet.

## Devpost Copy

Use `DEVPOST_SUBMISSION.md` as the full copy-ready draft.

- [ ] Paste the final code repository URL.
- [ ] Paste the final demo video URL.

**Project name:**

```text
Context
```

**Elevator pitch:**

```text
Text your workspace. Context reads GitHub, Notion, and Calendar, then tells you what changed and what to do next.
```

**Core sentence:**

```text
You do not search your workspace anymore. You text it.
```

## Repo Must Show

- [ ] `README.md` explains what works.
- [ ] `README.md` highlights Codex and GPT-5.6 usage.
- [ ] Repository URL points to the project code repo, not the demo workspace repo.
- [ ] `DEMO.md` includes the judge demo flow.
- [ ] `DEVPOST_SUBMISSION.md` is copy-ready.
- [ ] `backend/.env` is not committed.
- [ ] Xcode `xcuserdata` is not committed.

## Final Backend Checks

```bash
cd backend
source .venv/bin/activate
python -m pytest -q
./scripts/smoke.sh
```

## Final iOS Checks

```text
Open Context.xcodeproj
Select scheme Context
Select iPhone 17 Pro
Cmd+R
```

```

### DEMO.md

```markdown
# Context Demo Script

## One-Liner

You do not search your workspace anymore. You text it.

## Judge Story

Context is an AI teammate inside a native iOS chat. It reads GitHub, Notion, and Calendar, then answers like a concise coworker with tappable sources and safe action drafts.

## Setup Checklist

1. Start the backend:

```bash
cd /Users/rafael/Documents/Codex/2026-07-14/project-prompt-context-your-ai-teammate/Context/backend
source .venv/bin/activate
python -m uvicorn app.main:app --reload --reload-dir app
```

2. Verify the backend:

```bash
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/connections
curl http://127.0.0.1:8000/brief
```

3. Run the iOS app:

```text
Open Context.xcodeproj → scheme Context → iPhone 17 Pro → Cmd+R
```

## Demo Flow

### 1. Open App

Show the app opening directly into a conversation. No dashboard, no sidebar, no setup maze.

Say:

> Context feels like texting a coworker who already knows what happened at work.

### 2. Morning Brief

Let the first Context message appear.

Expected idea:

```text
Morning.
Here's what changed while you were away...
```

Point out:

- It reads workspace sources.
- It stays concise.
- Source cards are tappable.

### 3. Ask Priority Question

Tap or type:

```text
What should I work on first?
```

Expected behavior:

```text
Searching GitHub...
Reading Notion...
Checking Calendar...
Thinking...
```

Then Context replies with a prioritized answer.

Say:

> Instead of checking GitHub, docs, and calendar manually, I ask one question.

### 4. Tap Sources

Tap a GitHub, Notion, or Calendar source card.

Show:

- Native sheet.
- Source title.
- Source detail.
- Open link when available.

Say:

> Every answer includes evidence, so it does not feel like a black box.

### 5. Show Action Draft

Tap `Draft comment` or `Draft issue`.

Show:

- Context drafts the next action.
- Nothing is created until confirmed.
- The action has a safety boundary.

Say:

> Context can move from understanding to action, but it asks before doing anything.

### Closing Line

Return to the chat and hold the screen for three seconds.

Say:

> Context does not add another dashboard. It replaces the dashboard with a single question and a single next action.

### 6. Connect Workspace Sheet

Tap the plus button.

Show:

- Runtime setup fields for GitHub repo, Notion page, Calendar ICS, and Slack.
- GitHub / Notion / Calendar connection states.
- Demo fallback for unconfigured services.

Say:

> The interface stays conversational. Setup is just enough to point Context at your workspace; the product is still the chat.

## Backup Terminal Demo

If the simulator misbehaves, show the backend stream:

```bash
curl -N -X POST http://127.0.0.1:8000/chat \
  -H 'Content-Type: application/json' \
  -d '{"message":"What should I work on first?"}'
```

Expected SSE events:

```text
event: thinking_start
event: thinking_done
event: delta
event: sources
event: done
```

## Strongest Screenshots

Capture these:

1. Onboa
[truncated — 1145 more characters]
```

### backend/requirements.txt

```
fastapi==0.115.6
uvicorn[standard]==0.34.0
groq==0.13.0
pydantic==2.10.4
pydantic-settings==2.7.0
python-dotenv==1.0.1
pytest==8.3.4
pytest-asyncio==0.25.2
httpx==0.28.1

```

### backend/app/main.py

```python
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response, StreamingResponse

from app.actions import (
    ActionUnavailable,
    create_github_issue,
    create_github_pr_comment,
    draft_calendar_event,
    draft_github_issue,
    draft_github_pr_comment,
)
from app.brief import build_daily_brief
from app.connections import ConnectionsResponse, get_connections
from app.config import settings
from app.github import _clean_repo
from app.models import (
    ChatRequest,
    CreatedAction,
    CreateGitHubIssueRequest,
    CreateGitHubPRCommentRequest,
    DailyBrief,
    DraftAction,
    DraftCalendarEventRequest,
    DraftGitHubIssueRequest,
    DraftGitHubPRCommentRequest,
    WorkspaceSetupRequest,
)
from app.orchestrator import stream_chat
from app.workspace import priority_sources


app = FastAPI(title="Context Backend", version="0.1.0")

origins = ["*"] if settings.cors_origins == "*" else [
    origin.strip() for origin in settings.cors_origins.split(",") if origin.strip()
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/")
async def root() -> dict:
    return {"name": "Context Backend", "status": "ok"}


@app.get("/favicon.ico", include_in_schema=False)
@app.get("/apple-touch-icon.png", include_in_schema=False)
@app.get("/apple-touch-icon-precomposed.png", include_in_schema=False)
async def empty_icon() -> Response:
    return Response(status_code=204)


@app.get("/health")
async def health() -> dict:
    return {
        "status": "ok",
        "llm": "groq" if settings.groq_api_key else "fallback",
        "model": settings.groq_model if settings.groq_api_key else None,
    }


@app.get("/sources")
async def sources() -> dict:
    return {"sources": [source.model_dump() for source in priority_sources()]}


@app.get("/connections", response_model=ConnectionsResponse)
async def connections() -> ConnectionsResponse:
    return get_connections()


@app.post("/workspace/setup", response_model=ConnectionsResponse)
async def workspace_setup(request: WorkspaceSetupRequest) -> ConnectionsResponse:
    github_repo = _clean_repo(request.github_repo)
    if github_repo:
        settings.github_repo = github_repo

    if request.notion_page_id.strip():
        settings.notion_page_id = request.notion_page_id.strip()
    if request.notion_page_title.strip():
        settings.notion_page_title = request.notion_page_title.strip()

    if request.calendar_ics_url.strip():
        settings.calendar_ics_url = request.calendar_ics_url.strip()

    if request.slack_channel_id.strip():
        settings.slack_channel_id = request.slack_channel_id.strip()
    if request.slack_channel_name.strip():
        settings.slack_channel_name = request.slack_channel_name.strip().lstrip("#")

    return get_connections()


@app.get("/brief", response_model=DailyBrief)
async def brief() -> DailyBrief:
    return await build_daily_brief()


@app.post("/chat")
async def chat(request: ChatRequest) -> StreamingResponse:
    return StreamingResponse(
        stream_chat(request),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",
        },
    )


@app.post("/actions/draft/github-issue", response_model=DraftAction)
async def draft_github_issue_action(request: DraftGitHubIssueRequest) -> DraftAction:
    return draft_github_issue(request)


@app.post("/actions/draft/calendar-event", response_model=DraftAction)
async def draft_calendar_event_action(request: DraftCalendarEventRequest) -> DraftAction:
    return draft_calendar_event(request)


@app.post("/actions/draft/github-pr-comment", response_model=DraftAction)
async def draft_github_pr_comment_action(request: DraftGitHubPRCommentRequest) -> DraftAction:
    return draft_github_pr_comment(request)


@app.post("/actions/create/github-issue", response_model=CreatedAction)
async def create_github_issue_action(request: CreateGitHubIssueRequest) -> CreatedAction:
    try:
        return await create_github_issue(request)
    except ActionUnavailable as error:
        raise HTTPException(status_code=409, detail=str(error)) from error


@app.post("/actions/create/github-pr-comment", response_model=CreatedAction)
async def create_github_pr_comment_action(request: CreateGitHubPRCommentRequest) -> CreatedAction:
    try:
        return await create_github_pr_comment(request)
    except ActionUnavailable as error:
        raise HTTPException(status_code=409, detail=str(error)) from error

```

### render.yaml

```yaml
services:
  - type: web
    name: context-backend
    env: python
    rootDir: backend
    buildCommand: pip install -r requirements.txt
    startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT
    envVars:
      - key: GROQ_API_KEY
        sync: false
      - key: GROQ_MODEL
        value: llama-3.3-70b-versatile
      - key: GITHUB_TOKEN
        sync: false
      - key: GITHUB_REPO
        sync: false
      - key: NOTION_TOKEN
        sync: false
      - key: NOTION_PAGE_ID
        sync: false
      - key: NOTION_PAGE_TITLE
        value: Launch notes
      - key: CALENDAR_ICS_URL
        sync: false
      - key: CALENDAR_ICS_PATH
        sync: false
      - key: APP_TIMEZONE
        value: Europe/Budapest
      - key: SLACK_BOT_TOKEN
        sync: false
      - key: SLACK_CHANNEL_ID
        sync: false
      - key: SLACK_CHANNEL_NAME
        value: team-updates
      - key: CORS_ORIGINS
        value: "*"
      - key: FALLBACK_DELAY_MS
        value: "180"

```

### Context/ContextApp.swift

```swift
//
//  ContextApp.swift
//  Context
//
//  Created by Rafael Ibayev on 2026. 07. 14..
//

import SwiftUI

@main
struct ContextApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

```

### Context/ContentView.swift

```swift
import SwiftUI

struct ContentView: View {
    @State private var showOnboarding = true

    var body: some View {
        ZStack {
            if showOnboarding {
                OnboardingView {
                    withAnimation(ContextAnimation.gentle) {
                        showOnboarding = false
                    }
                }
                .transition(
                    .asymmetric(
                        insertion: .opacity,
                        removal: .opacity.combined(with: .scale(scale: 1.015, anchor: .center))
                    )
                )
                .zIndex(1)
            } else {
                ChatView()
                    .transition(
                        .asymmetric(
                            insertion: .opacity.combined(with: .scale(scale: 0.985, anchor: .center)),
                            removal: .opacity
                        )
                    )
                    .zIndex(0)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

```

### Context/HapticManager.swift

```swift
import UIKit
import AudioToolbox

// MARK: - HapticManager
// Per HIG "Playing haptics": use system-provided patterns according to documented meanings.
// - impact.light   → discrete UI tap (source card, suggested prompt)
// - impact.medium  → sheet presentation (context detail opens)
// - notification.success → message sent successfully
// Do NOT play haptics in response to every frame/scroll — only discrete events.

enum HapticManager {

    // A discrete light tap — use for small interactive elements
    static func lightTap() {
        let gen = UIImpactFeedbackGenerator(style: .light)
        gen.impactOccurred()
    }

    static func keyboardTap() {
        let gen = UIImpactFeedbackGenerator(style: .soft)
        gen.impactOccurred(intensity: 0.45)
        AudioServicesPlaySystemSound(1104)
    }

    // A medium tap — use when a sheet or panel appears
    static func mediumTap() {
        let gen = UIImpactFeedbackGenerator(style: .medium)
        gen.impactOccurred()
    }

    // Notification success — use when a message is sent
    static func success() {
        let gen = UINotificationFeedbackGenerator()
        gen.notificationOccurred(.success)
        AudioServicesPlaySystemSound(1004)
    }

    static func receivedMessage() {
        let gen = UIImpactFeedbackGenerator(style: .light)
        gen.impactOccurred(intensity: 0.7)
        AudioServicesPlaySystemSound(1003)
    }

    // Rigid snap — use when source card is selected/deselected
    static func snap() {
        let gen = UIImpactFeedbackGenerator(style: .rigid)
        gen.impactOccurred()
    }
}

```

### Context/DesignSystem.swift

```swift
import SwiftUI

enum ContextColor {
    // User bubble — iMessage blue
    static let userBlue     = Color(red: 0.0,   green: 0.478, blue: 1.0)
    static let userBlueDark = Color(red: 0.04,  green: 0.518, blue: 1.0)

    // Backgrounds
    static let lightBackground = Color(red: 0.974, green: 0.974, blue: 0.979)
    static let darkBackground  = Color(red: 0.040, green: 0.040, blue: 0.048)

    // Bubbles
    static let lightBubble = Color(red: 0.936, green: 0.940, blue: 0.950)
    static let darkBubble  = Color(red: 0.108, green: 0.108, blue: 0.118)

    // Borders
    static let lightBorder = Color(red: 0.830, green: 0.830, blue: 0.854)
    static let darkBorder  = Color(red: 0.168, green: 0.168, blue: 0.178)

    // Status
    static let onlineGreen = Color(red: 0.196, green: 0.784, blue: 0.349)
    /// Shared "completion" green — thinking checkmarks, connected checkmarks.
    static let success = Color(red: 0.30, green: 0.72, blue: 0.40)

    // Accent gradient stops — used in header shimmer
    static let accentA = Color(red: 0.0,  green: 0.60, blue: 1.0)
    static let accentB = Color(red: 0.42, green: 0.22, blue: 0.98)
}

enum ContextSpacing {
    static let screen:          CGFloat = 14
    static let messageGap:      CGFloat = 9
    static let bubbleHorizontal: CGFloat = 14
    static let bubbleVertical:  CGFloat = 10
    static let sourceGap:       CGFloat = 7
}

enum ContextRadius {
    static let bubble:   CGFloat = 20
    static let source:   CGFloat = 13
    static let composer: CGFloat = 23
}

// MARK: - Animation presets
enum ContextAnimation {
    /// Fast spring — send button, icons
    static let snap   = Animation.spring(response: 0.26, dampingFraction: 0.80)
    /// Standard spring — message bubbles appearing
    static let bubble = Animation.spring(response: 0.46, dampingFraction: 0.82)
    /// Gentle spring — scroll, panels
    static let gentle = Animation.spring(response: 0.54, dampingFraction: 0.88)
    /// Slow fade — thinking steps
    static let fade   = Animation.easeInOut(duration: 0.22)
}

// MARK: - Service brand identity
// Single source of truth for how each service looks across the app
// (picker rows, chat chips, detail sheets). Keeps branding consistent and
// avoids scattered hardcoded switches.

enum ContextBrand {
    /// SF Symbol glyph for a service.
    static func glyph(for service: String) -> String {
        switch service {
        case "GitHub":   return "chevron.left.forwardslash.chevron.right"
        case "Notion":   return "doc.text.fill"
        case "Calendar": return "calendar"
        case "Slack":    return "message.fill"
        case "Linear":   return "square.stack.fill"
        case "Figma":    return "paintbrush.fill"
        default:         return "circle.fill"
        }
    }

    /// Brand fill color for a service.
    static func color(for service: String) -> Color {
        switch service {
        case "GitHub":   return Color(red: 0.12, green: 0.12, blue: 0.14)
        case "Notion":   return Color(red: 0.40, green: 0.40, blue: 0.44)
        case "Calendar": return ContextColor.userBlue
        case "Slack":    return Color(red: 0.60, green: 0.14, blue: 0.58)
        case "Linear":   return Color(red: 0.35, green: 0.30, blue: 0.90)
        case "Figma":    return Color(red: 0.86, green: 0.34, blue: 0.30)
        default:         return Color.secondary
        }
    }
}

```

### Context/MessageHistoryStore.swift

```swift
import Foundation

struct MessageHistoryStore {
    private let key = "context.messageHistory.v1"
    private let archiveKey = "context.conversationArchive.v1"
    private let maxMessages = 80
    private let maxConversations = 16
    private let defaults: UserDefaults

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
    }

    func load() -> [ChatMessage] {
        guard let data = defaults.data(forKey: key) else { return [] }

        do {
            return try JSONDecoder().decode([ChatMessage].self, from: data)
        } catch {
            defaults.removeObject(forKey: key)
            return []
        }
    }

    func save(_ messages: [ChatMessage]) {
        let recentMessages = Array(messages.suffix(maxMessages))

        do {
            let data = try JSONEncoder().encode(recentMessages)
            defaults.set(data, forKey: key)
        } catch {
            defaults.removeObject(forKey: key)
        }
    }

    func clear() {
        defaults.removeObject(forKey: key)
    }

    func archivedConversations() -> [StoredConversation] {
        guard let data = defaults.data(forKey: archiveKey) else { return [] }

        do {
            return try JSONDecoder().decode([StoredConversation].self, from: data)
                .sorted { $0.updatedAt > $1.updatedAt }
        } catch {
            defaults.removeObject(forKey: archiveKey)
            return []
        }
    }

    func archive(_ messages: [ChatMessage]) {
        let meaningfulMessages = messages.filter {
            !$0.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        }
        guard !meaningfulMessages.isEmpty else { return }

        var conversations = archivedConversations()
        let messageIDs = Set(meaningfulMessages.map(\.id))
        conversations.removeAll { conversation in
            Set(conversation.messages.map(\.id)) == messageIDs
        }

        let recentMessages = Array(meaningfulMessages.suffix(maxMessages))
        conversations.insert(
            StoredConversation(
                id: UUID(),
                messages: recentMessages,
                updatedAt: Date(),
                title: title(from: recentMessages),
                preview: preview(from: recentMessages)
            ),
            at: 0
        )

        saveArchive(Array(conversations.prefix(maxConversations)))
    }

    func deleteConversation(id: UUID) {
        let conversations = archivedConversations().filter { $0.id != id }
        saveArchive(conversations)
    }

    private func saveArchive(_ conversations: [StoredConversation]) {
        do {
            let data = try JSONEncoder().encode(conversations)
            defaults.set(data, forKey: archiveKey)
        } catch {
            defaults.removeObject(forKey: archiveKey)
        }
    }

    private func title(from messages: [ChatMessage]) -> String {
        let titleMessage = messages.first(where: { $0.author == .user }) ?? messages.first
        return clipped(
            titleMessage?.text
                .components(separatedBy: .newlines)
                .first?
                .trimmingCharacters(in: .whitespacesAndNewlines),
            fallback: "Workspace brief",
            limit: 44
        )
    }

    private func preview(from messages: [ChatMessage]) -> String {
        clipped(
            messages.last?.text
                .replacingOccurrences(of: "\n", with: " ")
                .trimmingCharacters(in: .whitespacesAndNewlines),
            fallback: "No preview",
            limit: 88
        )
    }

    private func clipped(_ value: String?, fallback: String, limit: Int) -> String {
        guard let value, !value.isEmpty else { return fallback }
        guard value.count > limit else { return value }
        let endIndex = value.index(value.startIndex, offsetBy: limit)
        return String(value[..<endIndex]).trimmingCharacters(in: .whitespacesAndNewlines) + "…"
    }
}

```

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