Vibecode BoltAI
track this build5 phases, 15 steps, beginner friendly0%A coding agent can one-shot a basic chat window that calls a few AI APIs. That reproduces only a small part of BoltAI. A credible replacement must remain fast with large histories and active streams, combine SwiftUI with purpose-built AppKit components where the framework falls short, and make focus, keyboard navigation, menus, windows, drag and drop, and global shortcuts behave like a real Mac app. It must also reproduce AI Workflows, Instant Chat, Instant Dictation, attachments, tools, provider-specific behavior, local persistence, sync recovery, and protected credentials. These are interacting product systems, not a collection of generated screens.
You are building a lean indie version of BoltAI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # BoltAI · indie build An AppKit application that streams from several providers behind one interface, keeps a searchable history that stays fast at ten thousand messages, handles attachments, reaches you anywhere through a global panel that captures the selected text, and reports what each conversation cost. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 with AppKit, not a web view | the whole reason to build this is native responsiveness at a history size where a web client stutters | | Streaming | URLSession with server-sent events per provider | streaming is the interaction, and a response that appears all at once feels broken | | Storage | SQLite with full text search | ten thousand messages is small for a database and large for anything that keeps them in memory | | Providers | One adapter per provider behind a single protocol | the value of bringing your own keys is switching, which only works if switching is one line | | Local models | An OpenAI-compatible local endpoint as another provider | a local model should be a provider choice rather than a separate mode | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 13 or newer** · free if you own one - Why: The point of this build is native AppKit behaviour, which exists nowhere else. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 13 or higher - [ ] **Xcode 16 with a Swift 6 toolchain** · free - Why: Builds the application and provides Instruments, which you will need for the streaming and scrolling work. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **API keys for at least two providers** · pay per token; a development session is typically cents - Why: Phase 3 proves the abstraction, and one provider does not. Two hosted plus a local endpoint is the real test. - Get it: Create an account with two providers and generate a key for each, setting a spend limit at the same time. - Verify: Both stream a response through the same code path - [ ] **Ollama with a model** · free, uses your own memory - Why: The third provider, and the one that proves the abstraction is real rather than two hosted APIs that happen to look alike. - Get it: Download from ollama.com and pull a model sized for your machine. - Verify: The local endpoint streams through the same code path - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it there is no notarization and a warning on other machines. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh xcrun notarytool submit Chat.zip --keychain-profile AC --wait ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. - AppKit-backed performance and responsiveness across large workspaces and conversation histories - carefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior - AI Workflows that capture context from other applications and return formatted results where the user is working - Instant Chat and Instant Dictation as reliable system-wide workflows rather than ordinary in-app screens - maintained provider compatibility, Cloud Sync, mobile continuity, imports, backup recovery, and protected credential portability If one of those is essential to you, that is the reason to keep paying for BoltAI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · BoltAI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a native Mac chat client for your own model keys, fast with a long history and quick to reach from anywhere to replace BoltAI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 with AppKit, not a web view · the whole reason to build this is native responsiveness at a history size where a web client stutters - Streaming: URLSession with server-sent events per provider · streaming is the interaction, and a response that appears all at once feels broken - Storage: SQLite with full text search · ten thousand messages is small for a database and large for anything that keeps them in memory - Providers: One adapter per provider behind a single protocol · the value of bringing your own keys is switching, which only works if switching is one line - Local models: An OpenAI-compatible local endpoint as another provider · a local model should be a provider choice rather than a separate mode ### Data model (create this before Phase 1) - providers(id, name, base_url, keychain_ref, default_model, enabled) - conversations(id, title, provider_id, model, created_at, updated_at, archived, system_prompt) - messages(id, conversation_id, ordinal, role, content, attachments_json, created_at, prompt_tokens, completion_tokens, cost_usd) - messages_fts · full text over message content, updated on insert - presets(id, name, system_prompt, model, temperature, capture_kind) · capture_kind is none, selection or clipboard - attachments(id, message_id, path, kind, bytes, extracted_text) - API keys live in the keychain and are referenced, never stored in the database. ### Phase 1 · Streaming that feels native Build: One provider, streaming over server-sent events, rendering tokens as they arrive. Parse and update off the main thread and coalesce updates so the text view is not re-laid out per token. Cancellation must stop the request, not just the display. Trap: Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. Done when: A long response streams smoothly and the window scrolls and resizes during it. Cancelling stops the network request, verified by watching that no further tokens are billed. A response of several thousand words does not degrade typing responsiveness in the input field. Rendering happens at frame rate rather than per token. Do not build yet: One provider, no history, no attachments. ### Phase 2 · History that stays fast Build: Persist conversations and messages. Load a conversation lazily rather than entirely. Full text search across every message. Generate a fixture of ten thousand messages before optimising, so the numbers are real. Trap: Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. Done when: A conversation of two thousand messages opens in well under a second. Search across ten thousand messages returns in well under a hundred milliseconds. Scrolling a long conversation stays smooth with stable memory. Switching between conversations does not accumulate memory across switches. Do not build yet: One provider still, no attachments or presets. ### Phase 3 · Several providers, one interface Build: A provider protocol covering streaming, models, token counts and errors. Adapters for two hosted providers and one local endpoint. Keys in the keychain. Switching provider mid-conversation is allowed and recorded on the message. Trap: Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. Done when: Two hosted providers and one local endpoint all stream through the same code path. Switching mid-conversation works and each message records which model produced it. An invalid key produces the same clear error shape from every provider. Keys are in the keychain and absent from the database, verified by inspecting it. Do not build yet: No attachments, presets or cost reporting yet. ### Phase 4 · Reaching it from anywhere Build: A global hotkey opening a floating panel. Presets that capture the current selection or the clipboard and apply a system prompt. The result can be copied or pasted back into the application you came from. Trap: Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. Done when: The hotkey opens the panel from any application and Escape returns focus. A clipboard preset works with no additional permission. A selection preset requests Accessibility at the point of use and names why. Pasting the result back inserts into the previously focused application and restores the pasteboard. Do not build yet: No cost reporting or attachments yet. ### Phase 5 · Attachments and cost Build: Attach images and text files, extracting text where the model cannot read the format directly. Record tokens and cost per message, show a running conversation total, and warn at a threshold you set. Trap: Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. Done when: An image attachment reaches a model that supports it and a clear error appears for one that does not. A text file is extracted and included, and the per-turn token count reflects it. Conversation cost is visible and matches the provider's usage figures within a small margin. Crossing the threshold produces a warning. Do not build yet: Distribution comes last. ### Phase 6 · Signing, distribution and what leaves the machine Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly which providers receive message content, and make the local-only configuration a documented path rather than an accident. Trap: A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The active provider is visible in the window at all times. A local-only configuration is documented and sends nothing, verified with networking disabled. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - The tuned native behaviour of the commercial application across menus, panels, drag and drop and keyboard navigation, which is where most of its work has gone. - Workflows that capture context from other applications and return formatted results in place, which is a substantial automation surface of its own. - Model Context Protocol server support at any depth, which is a protocol implementation rather than a feature. - Cross-device sync of conversations, which needs a server and an encryption story. ### README must contain - That streaming renders at frame rate rather than per token, and why that matters - That conversations page rather than loading whole, with the measured figures - That keys live in the keychain and never in the database - That Accessibility is requested only when a selection preset is used - Which provider receives message content, shown in the window rather than only documented ===== AGENTS.md ===== # Agent instructions · BoltAI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 with AppKit, not a web view, URLSession with server-sent events per provider, SQLite with full text search, One adapter per provider behind a single protocol, An OpenAI-compatible local endpoint as another provider. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. ===== BUILD_PLAN.md ===== # Build plan · BoltAI An AppKit application that streams from several providers behind one interface, keeps a searchable history that stays fast at ten thousand messages, handles attachments, reaches you anywhere through a global panel that captures the selected text, and reports what each conversation cost. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Streaming that feels native A response streams in smoothly and the window stays responsive while it does. ### Steps 1. Implement streaming for one provider Server-sent events over URLSession, parsed incrementally off the main thread. Files: `Providers/Stream.swift` 2. Coalesce updates to frame rate Buffer tokens, flush on a display link. This single decision is most of the perceived speed. Files: `ChatView.swift` 3. Make cancellation real Cancel the task and the request. Stopping the display while tokens keep arriving still costs money. Files: `Providers/Stream.swift` ### Done when - [ ] A long response streams while the window stays responsive - [ ] Cancelling stops the network request and further billing - [ ] Typing stays responsive during a long stream - [ ] Layout happens at frame rate rather than per token ### Watch out - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. ## Phase 2 · History that stays fast Ten thousand messages load, scroll and search without a pause. ### Steps 1. Persist and page Load the visible window of messages, not the whole conversation. Files: `Store.swift` 2. Add full text search Updated on insert. Search is how you actually navigate a large history. Files: `Search.swift` 3. Generate a large fixture Ten thousand messages across several conversations, before any optimisation. Files: `Fixtures.swift` ### Done when - [ ] A two thousand message conversation opens in under a second - [ ] Search over ten thousand messages returns in under a hundred milliseconds - [ ] Long conversation scrolling stays smooth with stable memory - [ ] Switching conversations does not leak memory ### Watch out - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. ## Phase 3 · Several providers, one interface Switching provider or model is one action and costs nothing structural. ### Steps 1. Define one provider protocol Streaming, model list, token usage and a normalised error type. Everything provider-specific stays inside the adapter. Files: `Providers/Provider.swift` 2. Write three adapters Two hosted and one local. The third proves the abstraction rather than just implementing it twice. Files: `Providers/` 3. Keys in the keychain only Referenced from the database, never stored in it. Inspect the database file to confirm. Files: `Keychain.swift` ### Done when - [ ] Three providers stream through one code path - [ ] Switching mid-conversation records the model per message - [ ] An invalid key gives the same error shape everywhere - [ ] No key material appears in the database ### Watch out - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. ## Phase 4 · Reaching it from anywhere A hotkey brings up a panel that already knows what you were looking at. ### Steps 1. Add the global hotkey and panel A non-activating panel so the previous application keeps its context. Files: `QuickPanel.swift` 2. Capture clipboard first, selection second Clipboard needs no permission. Request Accessibility only when a selection preset is used. Files: `Capture.swift` 3. Paste back and restore Write, send Cmd-V, restore the previous pasteboard after a delay if your value is still there. Files: `PasteBack.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] A clipboard preset needs no additional permission - [ ] A selection preset requests Accessibility at point of use with a reason - [ ] Pasting back restores the previous pasteboard ### Watch out - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. ## Phase 5 · Attachments and cost You can send a file and you always know what a conversation has cost. ### Steps 1. Handle attachments per capability Check the model's capability first and fail clearly rather than sending something it will reject. Files: `Attachments.swift` 2. Record tokens and cost per message Including context tokens, which is the number that explains the bill. Files: `Cost.swift` 3. Reconcile against the provider Compare your total with the provider's dashboard for the same window. Investigate a gap. Files: `Cost.swift` ### Done when - [ ] An image reaches a capable model and errors clearly on an incapable one - [ ] Per-turn token count includes context - [ ] Conversation cost matches the provider dashboard closely - [ ] A threshold crossing produces a warning ### Watch out - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. ## Not in this build - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. ## After v1, if you want it - Conversation branching, which is cheap once messages are ordinal rather than a chain - A prompt library shared as a file rather than a service - Per-provider cost comparison for the same prompt, which BYOK makes possible ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Conversations and messages. Contains everything you have discussed, so treat backups accordingly. DB_PATH=~/Library/Application Support/Chat/chat.db # Required. Which provider is active on launch. Local means nothing leaves the machine. DEFAULT_PROVIDER=local # Required. How often buffered tokens are flushed to the view. Frame rate, not token rate. STREAM_FLUSH_HZ=60 # Required. Messages loaded per page in a conversation. HISTORY_PAGE_SIZE=80 # Required. Conversation cost at which a warning appears. COST_WARN_USD=1.50 # Optional. Product phase only. SPARKLE_FEED_URL=https://example.com/appcast.xml
You are building a lean indie version of BoltAI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # BoltAI · indie build An AppKit application that streams from several providers behind one interface, keeps a searchable history that stays fast at ten thousand messages, handles attachments, reaches you anywhere through a global panel that captures the selected text, and reports what each conversation cost. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 with AppKit, not a web view | the whole reason to build this is native responsiveness at a history size where a web client stutters | | Streaming | URLSession with server-sent events per provider | streaming is the interaction, and a response that appears all at once feels broken | | Storage | SQLite with full text search | ten thousand messages is small for a database and large for anything that keeps them in memory | | Providers | One adapter per provider behind a single protocol | the value of bringing your own keys is switching, which only works if switching is one line | | Local models | An OpenAI-compatible local endpoint as another provider | a local model should be a provider choice rather than a separate mode | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 13 or newer** · free if you own one - Why: The point of this build is native AppKit behaviour, which exists nowhere else. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 13 or higher - [ ] **Xcode 16 with a Swift 6 toolchain** · free - Why: Builds the application and provides Instruments, which you will need for the streaming and scrolling work. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **API keys for at least two providers** · pay per token; a development session is typically cents - Why: Phase 3 proves the abstraction, and one provider does not. Two hosted plus a local endpoint is the real test. - Get it: Create an account with two providers and generate a key for each, setting a spend limit at the same time. - Verify: Both stream a response through the same code path - [ ] **Ollama with a model** · free, uses your own memory - Why: The third provider, and the one that proves the abstraction is real rather than two hosted APIs that happen to look alike. - Get it: Download from ollama.com and pull a model sized for your machine. - Verify: The local endpoint streams through the same code path - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it there is no notarization and a warning on other machines. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh xcrun notarytool submit Chat.zip --keychain-profile AC --wait ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. - AppKit-backed performance and responsiveness across large workspaces and conversation histories - carefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior - AI Workflows that capture context from other applications and return formatted results where the user is working - Instant Chat and Instant Dictation as reliable system-wide workflows rather than ordinary in-app screens - maintained provider compatibility, Cloud Sync, mobile continuity, imports, backup recovery, and protected credential portability If one of those is essential to you, that is the reason to keep paying for BoltAI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · BoltAI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a native Mac chat client for your own model keys, fast with a long history and quick to reach from anywhere to replace BoltAI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 with AppKit, not a web view · the whole reason to build this is native responsiveness at a history size where a web client stutters - Streaming: URLSession with server-sent events per provider · streaming is the interaction, and a response that appears all at once feels broken - Storage: SQLite with full text search · ten thousand messages is small for a database and large for anything that keeps them in memory - Providers: One adapter per provider behind a single protocol · the value of bringing your own keys is switching, which only works if switching is one line - Local models: An OpenAI-compatible local endpoint as another provider · a local model should be a provider choice rather than a separate mode ### Data model (create this before Phase 1) - providers(id, name, base_url, keychain_ref, default_model, enabled) - conversations(id, title, provider_id, model, created_at, updated_at, archived, system_prompt) - messages(id, conversation_id, ordinal, role, content, attachments_json, created_at, prompt_tokens, completion_tokens, cost_usd) - messages_fts · full text over message content, updated on insert - presets(id, name, system_prompt, model, temperature, capture_kind) · capture_kind is none, selection or clipboard - attachments(id, message_id, path, kind, bytes, extracted_text) - API keys live in the keychain and are referenced, never stored in the database. ### Phase 1 · Streaming that feels native Build: One provider, streaming over server-sent events, rendering tokens as they arrive. Parse and update off the main thread and coalesce updates so the text view is not re-laid out per token. Cancellation must stop the request, not just the display. Trap: Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. Done when: A long response streams smoothly and the window scrolls and resizes during it. Cancelling stops the network request, verified by watching that no further tokens are billed. A response of several thousand words does not degrade typing responsiveness in the input field. Rendering happens at frame rate rather than per token. Do not build yet: One provider, no history, no attachments. ### Phase 2 · History that stays fast Build: Persist conversations and messages. Load a conversation lazily rather than entirely. Full text search across every message. Generate a fixture of ten thousand messages before optimising, so the numbers are real. Trap: Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. Done when: A conversation of two thousand messages opens in well under a second. Search across ten thousand messages returns in well under a hundred milliseconds. Scrolling a long conversation stays smooth with stable memory. Switching between conversations does not accumulate memory across switches. Do not build yet: One provider still, no attachments or presets. ### Phase 3 · Several providers, one interface Build: A provider protocol covering streaming, models, token counts and errors. Adapters for two hosted providers and one local endpoint. Keys in the keychain. Switching provider mid-conversation is allowed and recorded on the message. Trap: Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. Done when: Two hosted providers and one local endpoint all stream through the same code path. Switching mid-conversation works and each message records which model produced it. An invalid key produces the same clear error shape from every provider. Keys are in the keychain and absent from the database, verified by inspecting it. Do not build yet: No attachments, presets or cost reporting yet. ### Phase 4 · Reaching it from anywhere Build: A global hotkey opening a floating panel. Presets that capture the current selection or the clipboard and apply a system prompt. The result can be copied or pasted back into the application you came from. Trap: Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. Done when: The hotkey opens the panel from any application and Escape returns focus. A clipboard preset works with no additional permission. A selection preset requests Accessibility at the point of use and names why. Pasting the result back inserts into the previously focused application and restores the pasteboard. Do not build yet: No cost reporting or attachments yet. ### Phase 5 · Attachments and cost Build: Attach images and text files, extracting text where the model cannot read the format directly. Record tokens and cost per message, show a running conversation total, and warn at a threshold you set. Trap: Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. Done when: An image attachment reaches a model that supports it and a clear error appears for one that does not. A text file is extracted and included, and the per-turn token count reflects it. Conversation cost is visible and matches the provider's usage figures within a small margin. Crossing the threshold produces a warning. Do not build yet: Distribution comes last. ### Phase 6 · Signing, distribution and what leaves the machine Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly which providers receive message content, and make the local-only configuration a documented path rather than an accident. Trap: A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The active provider is visible in the window at all times. A local-only configuration is documented and sends nothing, verified with networking disabled. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - The tuned native behaviour of the commercial application across menus, panels, drag and drop and keyboard navigation, which is where most of its work has gone. - Workflows that capture context from other applications and return formatted results in place, which is a substantial automation surface of its own. - Model Context Protocol server support at any depth, which is a protocol implementation rather than a feature. - Cross-device sync of conversations, which needs a server and an encryption story. ### README must contain - That streaming renders at frame rate rather than per token, and why that matters - That conversations page rather than loading whole, with the measured figures - That keys live in the keychain and never in the database - That Accessibility is requested only when a selection preset is used - Which provider receives message content, shown in the window rather than only documented ===== AGENTS.md ===== # Agent instructions · BoltAI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 with AppKit, not a web view, URLSession with server-sent events per provider, SQLite with full text search, One adapter per provider behind a single protocol, An OpenAI-compatible local endpoint as another provider. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. ===== BUILD_PLAN.md ===== # Build plan · BoltAI An AppKit application that streams from several providers behind one interface, keeps a searchable history that stays fast at ten thousand messages, handles attachments, reaches you anywhere through a global panel that captures the selected text, and reports what each conversation cost. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Streaming that feels native A response streams in smoothly and the window stays responsive while it does. ### Steps 1. Implement streaming for one provider Server-sent events over URLSession, parsed incrementally off the main thread. Files: `Providers/Stream.swift` 2. Coalesce updates to frame rate Buffer tokens, flush on a display link. This single decision is most of the perceived speed. Files: `ChatView.swift` 3. Make cancellation real Cancel the task and the request. Stopping the display while tokens keep arriving still costs money. Files: `Providers/Stream.swift` ### Done when - [ ] A long response streams while the window stays responsive - [ ] Cancelling stops the network request and further billing - [ ] Typing stays responsive during a long stream - [ ] Layout happens at frame rate rather than per token ### Watch out - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. ## Phase 2 · History that stays fast Ten thousand messages load, scroll and search without a pause. ### Steps 1. Persist and page Load the visible window of messages, not the whole conversation. Files: `Store.swift` 2. Add full text search Updated on insert. Search is how you actually navigate a large history. Files: `Search.swift` 3. Generate a large fixture Ten thousand messages across several conversations, before any optimisation. Files: `Fixtures.swift` ### Done when - [ ] A two thousand message conversation opens in under a second - [ ] Search over ten thousand messages returns in under a hundred milliseconds - [ ] Long conversation scrolling stays smooth with stable memory - [ ] Switching conversations does not leak memory ### Watch out - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. ## Phase 3 · Several providers, one interface Switching provider or model is one action and costs nothing structural. ### Steps 1. Define one provider protocol Streaming, model list, token usage and a normalised error type. Everything provider-specific stays inside the adapter. Files: `Providers/Provider.swift` 2. Write three adapters Two hosted and one local. The third proves the abstraction rather than just implementing it twice. Files: `Providers/` 3. Keys in the keychain only Referenced from the database, never stored in it. Inspect the database file to confirm. Files: `Keychain.swift` ### Done when - [ ] Three providers stream through one code path - [ ] Switching mid-conversation records the model per message - [ ] An invalid key gives the same error shape everywhere - [ ] No key material appears in the database ### Watch out - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. ## Phase 4 · Reaching it from anywhere A hotkey brings up a panel that already knows what you were looking at. ### Steps 1. Add the global hotkey and panel A non-activating panel so the previous application keeps its context. Files: `QuickPanel.swift` 2. Capture clipboard first, selection second Clipboard needs no permission. Request Accessibility only when a selection preset is used. Files: `Capture.swift` 3. Paste back and restore Write, send Cmd-V, restore the previous pasteboard after a delay if your value is still there. Files: `PasteBack.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] A clipboard preset needs no additional permission - [ ] A selection preset requests Accessibility at point of use with a reason - [ ] Pasting back restores the previous pasteboard ### Watch out - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. ## Phase 5 · Attachments and cost You can send a file and you always know what a conversation has cost. ### Steps 1. Handle attachments per capability Check the model's capability first and fail clearly rather than sending something it will reject. Files: `Attachments.swift` 2. Record tokens and cost per message Including context tokens, which is the number that explains the bill. Files: `Cost.swift` 3. Reconcile against the provider Compare your total with the provider's dashboard for the same window. Investigate a gap. Files: `Cost.swift` ### Done when - [ ] An image reaches a capable model and errors clearly on an incapable one - [ ] Per-turn token count includes context - [ ] Conversation cost matches the provider dashboard closely - [ ] A threshold crossing produces a warning ### Watch out - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. ## Not in this build - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. ## After v1, if you want it - Conversation branching, which is cheap once messages are ordinal rather than a chain - A prompt library shared as a file rather than a service - Per-provider cost comparison for the same prompt, which BYOK makes possible ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Conversations and messages. Contains everything you have discussed, so treat backups accordingly. DB_PATH=~/Library/Application Support/Chat/chat.db # Required. Which provider is active on launch. Local means nothing leaves the machine. DEFAULT_PROVIDER=local # Required. How often buffered tokens are flushed to the view. Frame rate, not token rate. STREAM_FLUSH_HZ=60 # Required. Messages loaded per page in a conversation. HISTORY_PAGE_SIZE=80 # Required. Conversation cost at which a warning appears. COST_WARN_USD=1.50 # Optional. Product phase only. SPARKLE_FEED_URL=https://example.com/appcast.xml
You are building a production product version of BoltAI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # BoltAI · product brief ## Problem A coding agent can one-shot a basic chat window that calls a few AI APIs. That reproduces only a small part of BoltAI. A credible replacement must remain fast with large histories and active streams, combine SwiftUI with purpose-built AppKit components where the framework falls short, and make focus, keyboard navigation, menus, windows, drag and drop, and global shortcuts behave like a real Mac app. It must also reproduce AI Workflows, Instant Chat, Instant Dictation, attachments, tools, provider-specific behavior, local persistence, sync recovery, and protected credentials. These are interacting product systems, not a collection of generated screens. ## Product outcome A chat client that stays responsive at a history size where web clients stutter, switches providers in one action, shows what each conversation costs, and makes it obvious at all times where your messages are going. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 13 or later - Xcode with a Swift 6 toolchain - API keys for the selected cloud providers - Ollama for optional local models - Accessibility and microphone permissions for system-wide workflows ## Explicit non-goals for v1 - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. - AppKit-backed performance and responsiveness across large workspaces and conversation histories - carefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior - AI Workflows that capture context from other applications and return formatted results where the user is working - Instant Chat and Instant Dictation as reliable system-wide workflows rather than ordinary in-app screens - maintained provider compatibility, Cloud Sync, mobile continuity, imports, backup recovery, and protected credential portability ## Success criteria - Streaming stays smooth with the window responsive on a long response - Search latency on the ten thousand message fixture has not regressed - No key material appears in the database - The active provider is visible in the window - A local-only configuration is verified to send nothing ===== BRIEF.md ===== # Build brief · BoltAI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a native Mac chat client for your own model keys, fast with a long history and quick to reach from anywhere to replace BoltAI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 with AppKit, not a web view · the whole reason to build this is native responsiveness at a history size where a web client stutters - Streaming: URLSession with server-sent events per provider · streaming is the interaction, and a response that appears all at once feels broken - Storage: SQLite with full text search · ten thousand messages is small for a database and large for anything that keeps them in memory - Providers: One adapter per provider behind a single protocol · the value of bringing your own keys is switching, which only works if switching is one line - Local models: An OpenAI-compatible local endpoint as another provider · a local model should be a provider choice rather than a separate mode ### Data model (create this before Phase 1) - providers(id, name, base_url, keychain_ref, default_model, enabled) - conversations(id, title, provider_id, model, created_at, updated_at, archived, system_prompt) - messages(id, conversation_id, ordinal, role, content, attachments_json, created_at, prompt_tokens, completion_tokens, cost_usd) - messages_fts · full text over message content, updated on insert - presets(id, name, system_prompt, model, temperature, capture_kind) · capture_kind is none, selection or clipboard - attachments(id, message_id, path, kind, bytes, extracted_text) - API keys live in the keychain and are referenced, never stored in the database. ### Phase 1 · Streaming that feels native Build: One provider, streaming over server-sent events, rendering tokens as they arrive. Parse and update off the main thread and coalesce updates so the text view is not re-laid out per token. Cancellation must stop the request, not just the display. Trap: Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. Done when: A long response streams smoothly and the window scrolls and resizes during it. Cancelling stops the network request, verified by watching that no further tokens are billed. A response of several thousand words does not degrade typing responsiveness in the input field. Rendering happens at frame rate rather than per token. Do not build yet: One provider, no history, no attachments. ### Phase 2 · History that stays fast Build: Persist conversations and messages. Load a conversation lazily rather than entirely. Full text search across every message. Generate a fixture of ten thousand messages before optimising, so the numbers are real. Trap: Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. Done when: A conversation of two thousand messages opens in well under a second. Search across ten thousand messages returns in well under a hundred milliseconds. Scrolling a long conversation stays smooth with stable memory. Switching between conversations does not accumulate memory across switches. Do not build yet: One provider still, no attachments or presets. ### Phase 3 · Several providers, one interface Build: A provider protocol covering streaming, models, token counts and errors. Adapters for two hosted providers and one local endpoint. Keys in the keychain. Switching provider mid-conversation is allowed and recorded on the message. Trap: Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. Done when: Two hosted providers and one local endpoint all stream through the same code path. Switching mid-conversation works and each message records which model produced it. An invalid key produces the same clear error shape from every provider. Keys are in the keychain and absent from the database, verified by inspecting it. Do not build yet: No attachments, presets or cost reporting yet. ### Phase 4 · Reaching it from anywhere Build: A global hotkey opening a floating panel. Presets that capture the current selection or the clipboard and apply a system prompt. The result can be copied or pasted back into the application you came from. Trap: Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. Done when: The hotkey opens the panel from any application and Escape returns focus. A clipboard preset works with no additional permission. A selection preset requests Accessibility at the point of use and names why. Pasting the result back inserts into the previously focused application and restores the pasteboard. Do not build yet: No cost reporting or attachments yet. ### Phase 5 · Attachments and cost Build: Attach images and text files, extracting text where the model cannot read the format directly. Record tokens and cost per message, show a running conversation total, and warn at a threshold you set. Trap: Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. Done when: An image attachment reaches a model that supports it and a clear error appears for one that does not. A text file is extracted and included, and the per-turn token count reflects it. Conversation cost is visible and matches the provider's usage figures within a small margin. Crossing the threshold produces a warning. Do not build yet: Distribution comes last. ### Phase 6 · Signing, distribution and what leaves the machine Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly which providers receive message content, and make the local-only configuration a documented path rather than an accident. Trap: A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The active provider is visible in the window at all times. A local-only configuration is documented and sends nothing, verified with networking disabled. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - The tuned native behaviour of the commercial application across menus, panels, drag and drop and keyboard navigation, which is where most of its work has gone. - Workflows that capture context from other applications and return formatted results in place, which is a substantial automation surface of its own. - Model Context Protocol server support at any depth, which is a protocol implementation rather than a feature. - Cross-device sync of conversations, which needs a server and an encryption story. ### README must contain - That streaming renders at frame rate rather than per token, and why that matters - That conversations page rather than loading whole, with the measured figures - That keys live in the keychain and never in the database - That Accessibility is requested only when a selection preset is used - Which provider receives message content, shown in the window rather than only documented ===== ARCHITECTURE.md ===== # Architecture · BoltAI ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 with AppKit, not a web view | the whole reason to build this is native responsiveness at a history size where a web client stutters | | Streaming | URLSession with server-sent events per provider | streaming is the interaction, and a response that appears all at once feels broken | | Storage | SQLite with full text search | ten thousand messages is small for a database and large for anything that keeps them in memory | | Providers | One adapter per provider behind a single protocol | the value of bringing your own keys is switching, which only works if switching is one line | | Local models | An OpenAI-compatible local endpoint as another provider | a local model should be a provider choice rather than a separate mode | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Providers | Streaming, models, usage and normalised errors. | One protocol, several adapters. Everything provider-specific stays inside, which is what makes adding one cheap. | | Store | Conversations, messages and search. | SQLite is right at this size. The paging decision matters more than the engine. | | Chat view | Rendering and streaming performance. | The frame-rate flush is the decision that makes it feel native and should survive any rewrite. | | Quick panel | Global access and context capture. | Clipboard first, selection behind a permission requested at point of use. | | Cost | Tokens, spend and warnings. | Provider-specific pricing, so it needs updating when providers change rates. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Conversations and messages. Contains everything you have discussed, so treat backups accordingly. - `DEFAULT_PROVIDER` · required · Which provider is active on launch. Local means nothing leaves the machine. - `STREAM_FLUSH_HZ` · required · How often buffered tokens are flushed to the view. Frame rate, not token rate. - `HISTORY_PAGE_SIZE` · required · Messages loaded per page in a conversation. - `COST_WARN_USD` · required · Conversation cost at which a warning appears. - `SPARKLE_FEED_URL` · optional · Product phase only. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · BoltAI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 with AppKit, not a web view, URLSession with server-sent events per provider, SQLite with full text search, One adapter per provider behind a single protocol, An OpenAI-compatible local endpoint as another provider. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. - A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings. ===== MILESTONES.md ===== # Delivery milestones · BoltAI Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Streaming that feels native A response streams in smoothly and the window stays responsive while it does. ### Steps 1. Implement streaming for one provider Server-sent events over URLSession, parsed incrementally off the main thread. Files: `Providers/Stream.swift` 2. Coalesce updates to frame rate Buffer tokens, flush on a display link. This single decision is most of the perceived speed. Files: `ChatView.swift` 3. Make cancellation real Cancel the task and the request. Stopping the display while tokens keep arriving still costs money. Files: `Providers/Stream.swift` ### Done when - [ ] A long response streams while the window stays responsive - [ ] Cancelling stops the network request and further billing - [ ] Typing stays responsive during a long stream - [ ] Layout happens at frame rate rather than per token ### Watch out - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. ## M2 · History that stays fast Ten thousand messages load, scroll and search without a pause. ### Steps 1. Persist and page Load the visible window of messages, not the whole conversation. Files: `Store.swift` 2. Add full text search Updated on insert. Search is how you actually navigate a large history. Files: `Search.swift` 3. Generate a large fixture Ten thousand messages across several conversations, before any optimisation. Files: `Fixtures.swift` ### Done when - [ ] A two thousand message conversation opens in under a second - [ ] Search over ten thousand messages returns in under a hundred milliseconds - [ ] Long conversation scrolling stays smooth with stable memory - [ ] Switching conversations does not leak memory ### Watch out - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. ## M3 · Several providers, one interface Switching provider or model is one action and costs nothing structural. ### Steps 1. Define one provider protocol Streaming, model list, token usage and a normalised error type. Everything provider-specific stays inside the adapter. Files: `Providers/Provider.swift` 2. Write three adapters Two hosted and one local. The third proves the abstraction rather than just implementing it twice. Files: `Providers/` 3. Keys in the keychain only Referenced from the database, never stored in it. Inspect the database file to confirm. Files: `Keychain.swift` ### Done when - [ ] Three providers stream through one code path - [ ] Switching mid-conversation records the model per message - [ ] An invalid key gives the same error shape everywhere - [ ] No key material appears in the database ### Watch out - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. ## M4 · Reaching it from anywhere A hotkey brings up a panel that already knows what you were looking at. ### Steps 1. Add the global hotkey and panel A non-activating panel so the previous application keeps its context. Files: `QuickPanel.swift` 2. Capture clipboard first, selection second Clipboard needs no permission. Request Accessibility only when a selection preset is used. Files: `Capture.swift` 3. Paste back and restore Write, send Cmd-V, restore the previous pasteboard after a delay if your value is still there. Files: `PasteBack.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] A clipboard preset needs no additional permission - [ ] A selection preset requests Accessibility at point of use with a reason - [ ] Pasting back restores the previous pasteboard ### Watch out - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. ## M5 · Attachments and cost You can send a file and you always know what a conversation has cost. ### Steps 1. Handle attachments per capability Check the model's capability first and fail clearly rather than sending something it will reject. Files: `Attachments.swift` 2. Record tokens and cost per message Including context tokens, which is the number that explains the bill. Files: `Cost.swift` 3. Reconcile against the provider Compare your total with the provider's dashboard for the same window. Investigate a gap. Files: `Cost.swift` ### Done when - [ ] An image reaches a capable model and errors clearly on an incapable one - [ ] Per-turn token count includes context - [ ] Conversation cost matches the provider dashboard closely - [ ] A threshold crossing produces a warning ### Watch out - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. ## M6 · Signing, distribution and what leaves the machine (production only) Others can install it and know exactly where their conversations go. ### Steps 1. Sign and notarize Hardened runtime, Developer ID, notarytool, stapler. ```sh xcrun notarytool submit Chat.zip --keychain-profile AC --wait ``` 2. Show the active provider always In the window, not buried in settings. It is the most important fact on screen. Files: `ProviderBadge.swift` 3. Document the local-only path A configuration that sends nothing, verified with networking off. Files: `README.md` ### Done when - [ ] spctl reports the notarized build accepted - [ ] The active provider is visible in the window - [ ] A local-only configuration works with networking disabled - [ ] An appcast update installs and relaunches ### Watch out - A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings. ===== OPERATIONS.md ===== # Operations · BoltAI ## Backup The database contains every conversation, which for many people includes work they would not want disclosed. Back it up encrypted, and consider a retention policy rather than keeping everything forever. ## Restore Restore the database and the keychain entries. Without the keys the history is readable and the application cannot send anything, which is a reasonable degraded state. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing runs as a service. Track streaming smoothness and search latency against the ten thousand message fixture across releases, since both degrade quietly as features accumulate. ## Incident checklist A provider changing its streaming format or pricing is the recurring maintenance. Because both are inside adapters, the blast radius is one file, which is the reason for the abstraction. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Streaming stays smooth with the window responsive on a long response - [ ] Search latency on the ten thousand message fixture has not regressed - [ ] No key material appears in the database - [ ] The active provider is visible in the window - [ ] A local-only configuration is verified to send nothing ## Launch constraint Do not market omitted BoltAI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Conversations and messages. Contains everything you have discussed, so treat backups accordingly. DB_PATH=~/Library/Application Support/Chat/chat.db # Required. Which provider is active on launch. Local means nothing leaves the machine. DEFAULT_PROVIDER=local # Required. How often buffered tokens are flushed to the view. Frame rate, not token rate. STREAM_FLUSH_HZ=60 # Required. Messages loaded per page in a conversation. HISTORY_PAGE_SIZE=80 # Required. Conversation cost at which a warning appears. COST_WARN_USD=1.50 # Optional. Product phase only. SPARKLE_FEED_URL=https://example.com/appcast.xml
# BoltAI · indie build An AppKit application that streams from several providers behind one interface, keeps a searchable history that stays fast at ten thousand messages, handles attachments, reaches you anywhere through a global panel that captures the selected text, and reports what each conversation cost. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 with AppKit, not a web view | the whole reason to build this is native responsiveness at a history size where a web client stutters | | Streaming | URLSession with server-sent events per provider | streaming is the interaction, and a response that appears all at once feels broken | | Storage | SQLite with full text search | ten thousand messages is small for a database and large for anything that keeps them in memory | | Providers | One adapter per provider behind a single protocol | the value of bringing your own keys is switching, which only works if switching is one line | | Local models | An OpenAI-compatible local endpoint as another provider | a local model should be a provider choice rather than a separate mode | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 13 or newer** · free if you own one - Why: The point of this build is native AppKit behaviour, which exists nowhere else. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 13 or higher - [ ] **Xcode 16 with a Swift 6 toolchain** · free - Why: Builds the application and provides Instruments, which you will need for the streaming and scrolling work. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **API keys for at least two providers** · pay per token; a development session is typically cents - Why: Phase 3 proves the abstraction, and one provider does not. Two hosted plus a local endpoint is the real test. - Get it: Create an account with two providers and generate a key for each, setting a spend limit at the same time. - Verify: Both stream a response through the same code path - [ ] **Ollama with a model** · free, uses your own memory - Why: The third provider, and the one that proves the abstraction is real rather than two hosted APIs that happen to look alike. - Get it: Download from ollama.com and pull a model sized for your machine. - Verify: The local endpoint streams through the same code path - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it there is no notarization and a warning on other machines. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh xcrun notarytool submit Chat.zip --keychain-profile AC --wait ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. - AppKit-backed performance and responsiveness across large workspaces and conversation histories - carefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior - AI Workflows that capture context from other applications and return formatted results where the user is working - Instant Chat and Instant Dictation as reliable system-wide workflows rather than ordinary in-app screens - maintained provider compatibility, Cloud Sync, mobile continuity, imports, backup recovery, and protected credential portability If one of those is essential to you, that is the reason to keep paying for BoltAI, and the README should say so rather than pretend.
# Build brief · BoltAI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a native Mac chat client for your own model keys, fast with a long history and quick to reach from anywhere to replace BoltAI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 with AppKit, not a web view · the whole reason to build this is native responsiveness at a history size where a web client stutters - Streaming: URLSession with server-sent events per provider · streaming is the interaction, and a response that appears all at once feels broken - Storage: SQLite with full text search · ten thousand messages is small for a database and large for anything that keeps them in memory - Providers: One adapter per provider behind a single protocol · the value of bringing your own keys is switching, which only works if switching is one line - Local models: An OpenAI-compatible local endpoint as another provider · a local model should be a provider choice rather than a separate mode ### Data model (create this before Phase 1) - providers(id, name, base_url, keychain_ref, default_model, enabled) - conversations(id, title, provider_id, model, created_at, updated_at, archived, system_prompt) - messages(id, conversation_id, ordinal, role, content, attachments_json, created_at, prompt_tokens, completion_tokens, cost_usd) - messages_fts · full text over message content, updated on insert - presets(id, name, system_prompt, model, temperature, capture_kind) · capture_kind is none, selection or clipboard - attachments(id, message_id, path, kind, bytes, extracted_text) - API keys live in the keychain and are referenced, never stored in the database. ### Phase 1 · Streaming that feels native Build: One provider, streaming over server-sent events, rendering tokens as they arrive. Parse and update off the main thread and coalesce updates so the text view is not re-laid out per token. Cancellation must stop the request, not just the display. Trap: Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. Done when: A long response streams smoothly and the window scrolls and resizes during it. Cancelling stops the network request, verified by watching that no further tokens are billed. A response of several thousand words does not degrade typing responsiveness in the input field. Rendering happens at frame rate rather than per token. Do not build yet: One provider, no history, no attachments. ### Phase 2 · History that stays fast Build: Persist conversations and messages. Load a conversation lazily rather than entirely. Full text search across every message. Generate a fixture of ten thousand messages before optimising, so the numbers are real. Trap: Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. Done when: A conversation of two thousand messages opens in well under a second. Search across ten thousand messages returns in well under a hundred milliseconds. Scrolling a long conversation stays smooth with stable memory. Switching between conversations does not accumulate memory across switches. Do not build yet: One provider still, no attachments or presets. ### Phase 3 · Several providers, one interface Build: A provider protocol covering streaming, models, token counts and errors. Adapters for two hosted providers and one local endpoint. Keys in the keychain. Switching provider mid-conversation is allowed and recorded on the message. Trap: Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. Done when: Two hosted providers and one local endpoint all stream through the same code path. Switching mid-conversation works and each message records which model produced it. An invalid key produces the same clear error shape from every provider. Keys are in the keychain and absent from the database, verified by inspecting it. Do not build yet: No attachments, presets or cost reporting yet. ### Phase 4 · Reaching it from anywhere Build: A global hotkey opening a floating panel. Presets that capture the current selection or the clipboard and apply a system prompt. The result can be copied or pasted back into the application you came from. Trap: Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. Done when: The hotkey opens the panel from any application and Escape returns focus. A clipboard preset works with no additional permission. A selection preset requests Accessibility at the point of use and names why. Pasting the result back inserts into the previously focused application and restores the pasteboard. Do not build yet: No cost reporting or attachments yet. ### Phase 5 · Attachments and cost Build: Attach images and text files, extracting text where the model cannot read the format directly. Record tokens and cost per message, show a running conversation total, and warn at a threshold you set. Trap: Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. Done when: An image attachment reaches a model that supports it and a clear error appears for one that does not. A text file is extracted and included, and the per-turn token count reflects it. Conversation cost is visible and matches the provider's usage figures within a small margin. Crossing the threshold produces a warning. Do not build yet: Distribution comes last. ### Phase 6 · Signing, distribution and what leaves the machine Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly which providers receive message content, and make the local-only configuration a documented path rather than an accident. Trap: A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The active provider is visible in the window at all times. A local-only configuration is documented and sends nothing, verified with networking disabled. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - The tuned native behaviour of the commercial application across menus, panels, drag and drop and keyboard navigation, which is where most of its work has gone. - Workflows that capture context from other applications and return formatted results in place, which is a substantial automation surface of its own. - Model Context Protocol server support at any depth, which is a protocol implementation rather than a feature. - Cross-device sync of conversations, which needs a server and an encryption story. ### README must contain - That streaming renders at frame rate rather than per token, and why that matters - That conversations page rather than loading whole, with the measured figures - That keys live in the keychain and never in the database - That Accessibility is requested only when a selection preset is used - Which provider receives message content, shown in the window rather than only documented
# Agent instructions · BoltAI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 with AppKit, not a web view, URLSession with server-sent events per provider, SQLite with full text search, One adapter per provider behind a single protocol, An OpenAI-compatible local endpoint as another provider. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible.
# Build plan · BoltAI An AppKit application that streams from several providers behind one interface, keeps a searchable history that stays fast at ten thousand messages, handles attachments, reaches you anywhere through a global panel that captures the selected text, and reports what each conversation cost. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Streaming that feels native A response streams in smoothly and the window stays responsive while it does. ### Steps 1. Implement streaming for one provider Server-sent events over URLSession, parsed incrementally off the main thread. Files: `Providers/Stream.swift` 2. Coalesce updates to frame rate Buffer tokens, flush on a display link. This single decision is most of the perceived speed. Files: `ChatView.swift` 3. Make cancellation real Cancel the task and the request. Stopping the display while tokens keep arriving still costs money. Files: `Providers/Stream.swift` ### Done when - [ ] A long response streams while the window stays responsive - [ ] Cancelling stops the network request and further billing - [ ] Typing stays responsive during a long stream - [ ] Layout happens at frame rate rather than per token ### Watch out - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. ## Phase 2 · History that stays fast Ten thousand messages load, scroll and search without a pause. ### Steps 1. Persist and page Load the visible window of messages, not the whole conversation. Files: `Store.swift` 2. Add full text search Updated on insert. Search is how you actually navigate a large history. Files: `Search.swift` 3. Generate a large fixture Ten thousand messages across several conversations, before any optimisation. Files: `Fixtures.swift` ### Done when - [ ] A two thousand message conversation opens in under a second - [ ] Search over ten thousand messages returns in under a hundred milliseconds - [ ] Long conversation scrolling stays smooth with stable memory - [ ] Switching conversations does not leak memory ### Watch out - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. ## Phase 3 · Several providers, one interface Switching provider or model is one action and costs nothing structural. ### Steps 1. Define one provider protocol Streaming, model list, token usage and a normalised error type. Everything provider-specific stays inside the adapter. Files: `Providers/Provider.swift` 2. Write three adapters Two hosted and one local. The third proves the abstraction rather than just implementing it twice. Files: `Providers/` 3. Keys in the keychain only Referenced from the database, never stored in it. Inspect the database file to confirm. Files: `Keychain.swift` ### Done when - [ ] Three providers stream through one code path - [ ] Switching mid-conversation records the model per message - [ ] An invalid key gives the same error shape everywhere - [ ] No key material appears in the database ### Watch out - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. ## Phase 4 · Reaching it from anywhere A hotkey brings up a panel that already knows what you were looking at. ### Steps 1. Add the global hotkey and panel A non-activating panel so the previous application keeps its context. Files: `QuickPanel.swift` 2. Capture clipboard first, selection second Clipboard needs no permission. Request Accessibility only when a selection preset is used. Files: `Capture.swift` 3. Paste back and restore Write, send Cmd-V, restore the previous pasteboard after a delay if your value is still there. Files: `PasteBack.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] A clipboard preset needs no additional permission - [ ] A selection preset requests Accessibility at point of use with a reason - [ ] Pasting back restores the previous pasteboard ### Watch out - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. ## Phase 5 · Attachments and cost You can send a file and you always know what a conversation has cost. ### Steps 1. Handle attachments per capability Check the model's capability first and fail clearly rather than sending something it will reject. Files: `Attachments.swift` 2. Record tokens and cost per message Including context tokens, which is the number that explains the bill. Files: `Cost.swift` 3. Reconcile against the provider Compare your total with the provider's dashboard for the same window. Investigate a gap. Files: `Cost.swift` ### Done when - [ ] An image reaches a capable model and errors clearly on an incapable one - [ ] Per-turn token count includes context - [ ] Conversation cost matches the provider dashboard closely - [ ] A threshold crossing produces a warning ### Watch out - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. ## Not in this build - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. ## After v1, if you want it - Conversation branching, which is cheap once messages are ordinal rather than a chain - A prompt library shared as a file rather than a service - Per-provider cost comparison for the same prompt, which BYOK makes possible
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Conversations and messages. Contains everything you have discussed, so treat backups accordingly. DB_PATH=~/Library/Application Support/Chat/chat.db # Required. Which provider is active on launch. Local means nothing leaves the machine. DEFAULT_PROVIDER=local # Required. How often buffered tokens are flushed to the view. Frame rate, not token rate. STREAM_FLUSH_HZ=60 # Required. Messages loaded per page in a conversation. HISTORY_PAGE_SIZE=80 # Required. Conversation cost at which a warning appears. COST_WARN_USD=1.50 # Optional. Product phase only. SPARKLE_FEED_URL=https://example.com/appcast.xml
# BoltAI · product brief ## Problem A coding agent can one-shot a basic chat window that calls a few AI APIs. That reproduces only a small part of BoltAI. A credible replacement must remain fast with large histories and active streams, combine SwiftUI with purpose-built AppKit components where the framework falls short, and make focus, keyboard navigation, menus, windows, drag and drop, and global shortcuts behave like a real Mac app. It must also reproduce AI Workflows, Instant Chat, Instant Dictation, attachments, tools, provider-specific behavior, local persistence, sync recovery, and protected credentials. These are interacting product systems, not a collection of generated screens. ## Product outcome A chat client that stays responsive at a history size where web clients stutter, switches providers in one action, shows what each conversation costs, and makes it obvious at all times where your messages are going. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 13 or later - Xcode with a Swift 6 toolchain - API keys for the selected cloud providers - Ollama for optional local models - Accessibility and microphone permissions for system-wide workflows ## Explicit non-goals for v1 - The tuned native behaviour of the commercial application. - Context-capturing workflows that return results in place. - Model Context Protocol server support. - Cross-device conversation sync. - AppKit-backed performance and responsiveness across large workspaces and conversation histories - carefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior - AI Workflows that capture context from other applications and return formatted results where the user is working - Instant Chat and Instant Dictation as reliable system-wide workflows rather than ordinary in-app screens - maintained provider compatibility, Cloud Sync, mobile continuity, imports, backup recovery, and protected credential portability ## Success criteria - Streaming stays smooth with the window responsive on a long response - Search latency on the ten thousand message fixture has not regressed - No key material appears in the database - The active provider is visible in the window - A local-only configuration is verified to send nothing
# Build brief · BoltAI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a native Mac chat client for your own model keys, fast with a long history and quick to reach from anywhere to replace BoltAI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 with AppKit, not a web view · the whole reason to build this is native responsiveness at a history size where a web client stutters - Streaming: URLSession with server-sent events per provider · streaming is the interaction, and a response that appears all at once feels broken - Storage: SQLite with full text search · ten thousand messages is small for a database and large for anything that keeps them in memory - Providers: One adapter per provider behind a single protocol · the value of bringing your own keys is switching, which only works if switching is one line - Local models: An OpenAI-compatible local endpoint as another provider · a local model should be a provider choice rather than a separate mode ### Data model (create this before Phase 1) - providers(id, name, base_url, keychain_ref, default_model, enabled) - conversations(id, title, provider_id, model, created_at, updated_at, archived, system_prompt) - messages(id, conversation_id, ordinal, role, content, attachments_json, created_at, prompt_tokens, completion_tokens, cost_usd) - messages_fts · full text over message content, updated on insert - presets(id, name, system_prompt, model, temperature, capture_kind) · capture_kind is none, selection or clipboard - attachments(id, message_id, path, kind, bytes, extracted_text) - API keys live in the keychain and are referenced, never stored in the database. ### Phase 1 · Streaming that feels native Build: One provider, streaming over server-sent events, rendering tokens as they arrive. Parse and update off the main thread and coalesce updates so the text view is not re-laid out per token. Cancellation must stop the request, not just the display. Trap: Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. Done when: A long response streams smoothly and the window scrolls and resizes during it. Cancelling stops the network request, verified by watching that no further tokens are billed. A response of several thousand words does not degrade typing responsiveness in the input field. Rendering happens at frame rate rather than per token. Do not build yet: One provider, no history, no attachments. ### Phase 2 · History that stays fast Build: Persist conversations and messages. Load a conversation lazily rather than entirely. Full text search across every message. Generate a fixture of ten thousand messages before optimising, so the numbers are real. Trap: Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. Done when: A conversation of two thousand messages opens in well under a second. Search across ten thousand messages returns in well under a hundred milliseconds. Scrolling a long conversation stays smooth with stable memory. Switching between conversations does not accumulate memory across switches. Do not build yet: One provider still, no attachments or presets. ### Phase 3 · Several providers, one interface Build: A provider protocol covering streaming, models, token counts and errors. Adapters for two hosted providers and one local endpoint. Keys in the keychain. Switching provider mid-conversation is allowed and recorded on the message. Trap: Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. Done when: Two hosted providers and one local endpoint all stream through the same code path. Switching mid-conversation works and each message records which model produced it. An invalid key produces the same clear error shape from every provider. Keys are in the keychain and absent from the database, verified by inspecting it. Do not build yet: No attachments, presets or cost reporting yet. ### Phase 4 · Reaching it from anywhere Build: A global hotkey opening a floating panel. Presets that capture the current selection or the clipboard and apply a system prompt. The result can be copied or pasted back into the application you came from. Trap: Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. Done when: The hotkey opens the panel from any application and Escape returns focus. A clipboard preset works with no additional permission. A selection preset requests Accessibility at the point of use and names why. Pasting the result back inserts into the previously focused application and restores the pasteboard. Do not build yet: No cost reporting or attachments yet. ### Phase 5 · Attachments and cost Build: Attach images and text files, extracting text where the model cannot read the format directly. Record tokens and cost per message, show a running conversation total, and warn at a threshold you set. Trap: Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. Done when: An image attachment reaches a model that supports it and a clear error appears for one that does not. A text file is extracted and included, and the per-turn token count reflects it. Conversation cost is visible and matches the provider's usage figures within a small margin. Crossing the threshold produces a warning. Do not build yet: Distribution comes last. ### Phase 6 · Signing, distribution and what leaves the machine Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. State plainly which providers receive message content, and make the local-only configuration a documented path rather than an accident. Trap: A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. The active provider is visible in the window at all times. A local-only configuration is documented and sends nothing, verified with networking disabled. An appcast update installs and relaunches. Do not build yet: Nothing after this. ### Out of scope (and why) - The tuned native behaviour of the commercial application across menus, panels, drag and drop and keyboard navigation, which is where most of its work has gone. - Workflows that capture context from other applications and return formatted results in place, which is a substantial automation surface of its own. - Model Context Protocol server support at any depth, which is a protocol implementation rather than a feature. - Cross-device sync of conversations, which needs a server and an encryption story. ### README must contain - That streaming renders at frame rate rather than per token, and why that matters - That conversations page rather than loading whole, with the measured figures - That keys live in the keychain and never in the database - That Accessibility is requested only when a selection preset is used - Which provider receives message content, shown in the window rather than only documented
# Architecture · BoltAI ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 with AppKit, not a web view | the whole reason to build this is native responsiveness at a history size where a web client stutters | | Streaming | URLSession with server-sent events per provider | streaming is the interaction, and a response that appears all at once feels broken | | Storage | SQLite with full text search | ten thousand messages is small for a database and large for anything that keeps them in memory | | Providers | One adapter per provider behind a single protocol | the value of bringing your own keys is switching, which only works if switching is one line | | Local models | An OpenAI-compatible local endpoint as another provider | a local model should be a provider choice rather than a separate mode | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Providers | Streaming, models, usage and normalised errors. | One protocol, several adapters. Everything provider-specific stays inside, which is what makes adding one cheap. | | Store | Conversations, messages and search. | SQLite is right at this size. The paging decision matters more than the engine. | | Chat view | Rendering and streaming performance. | The frame-rate flush is the decision that makes it feel native and should survive any rewrite. | | Quick panel | Global access and context capture. | Clipboard first, selection behind a permission requested at point of use. | | Cost | Tokens, spend and warnings. | Provider-specific pricing, so it needs updating when providers change rates. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Conversations and messages. Contains everything you have discussed, so treat backups accordingly. - `DEFAULT_PROVIDER` · required · Which provider is active on launch. Local means nothing leaves the machine. - `STREAM_FLUSH_HZ` · required · How often buffered tokens are flushed to the view. Frame rate, not token rate. - `HISTORY_PAGE_SIZE` · required · Messages loaded per page in a conversation. - `COST_WARN_USD` · required · Conversation cost at which a warning appears. - `SPARKLE_FEED_URL` · optional · Product phase only. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · BoltAI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 with AppKit, not a web view, URLSession with server-sent events per provider, SQLite with full text search, One adapter per provider behind a single protocol, An OpenAI-compatible local endpoint as another provider. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. - A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings.
# Delivery milestones · BoltAI Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Streaming that feels native A response streams in smoothly and the window stays responsive while it does. ### Steps 1. Implement streaming for one provider Server-sent events over URLSession, parsed incrementally off the main thread. Files: `Providers/Stream.swift` 2. Coalesce updates to frame rate Buffer tokens, flush on a display link. This single decision is most of the perceived speed. Files: `ChatView.swift` 3. Make cancellation real Cancel the task and the request. Stopping the display while tokens keep arriving still costs money. Files: `Providers/Stream.swift` ### Done when - [ ] A long response streams while the window stays responsive - [ ] Cancelling stops the network request and further billing - [ ] Typing stays responsive during a long stream - [ ] Layout happens at frame rate rather than per token ### Watch out - Appending each token directly to the text view forces a full layout pass per token, which pins the main thread and makes a native application feel worse than a web one. Buffer tokens and flush on a display-linked interval, so layout happens at frame rate rather than at token rate. ## M2 · History that stays fast Ten thousand messages load, scroll and search without a pause. ### Steps 1. Persist and page Load the visible window of messages, not the whole conversation. Files: `Store.swift` 2. Add full text search Updated on insert. Search is how you actually navigate a large history. Files: `Search.swift` 3. Generate a large fixture Ten thousand messages across several conversations, before any optimisation. Files: `Fixtures.swift` ### Done when - [ ] A two thousand message conversation opens in under a second - [ ] Search over ten thousand messages returns in under a hundred milliseconds - [ ] Long conversation scrolling stays smooth with stable memory - [ ] Switching conversations does not leak memory ### Watch out - Loading an entire conversation into a single text view works until a conversation is long, then costs seconds and hundreds of megabytes. Page the message list and render rows, which is what a native list view is for and is the reason to write this natively at all. ## M3 · Several providers, one interface Switching provider or model is one action and costs nothing structural. ### Steps 1. Define one provider protocol Streaming, model list, token usage and a normalised error type. Everything provider-specific stays inside the adapter. Files: `Providers/Provider.swift` 2. Write three adapters Two hosted and one local. The third proves the abstraction rather than just implementing it twice. Files: `Providers/` 3. Keys in the keychain only Referenced from the database, never stored in it. Inspect the database file to confirm. Files: `Keychain.swift` ### Done when - [ ] Three providers stream through one code path - [ ] Switching mid-conversation records the model per message - [ ] An invalid key gives the same error shape everywhere - [ ] No key material appears in the database ### Watch out - Providers differ in how they report errors, count tokens and format streaming chunks, and letting those differences leak into the interface layer means every provider addition touches the whole application. Normalise inside the adapter, including errors, so the rest of the application sees one shape. ## M4 · Reaching it from anywhere A hotkey brings up a panel that already knows what you were looking at. ### Steps 1. Add the global hotkey and panel A non-activating panel so the previous application keeps its context. Files: `QuickPanel.swift` 2. Capture clipboard first, selection second Clipboard needs no permission. Request Accessibility only when a selection preset is used. Files: `Capture.swift` 3. Paste back and restore Write, send Cmd-V, restore the previous pasteboard after a delay if your value is still there. Files: `PasteBack.swift` ### Done when - [ ] The hotkey opens the panel from any application - [ ] A clipboard preset needs no additional permission - [ ] A selection preset requests Accessibility at point of use with a reason - [ ] Pasting back restores the previous pasteboard ### Watch out - Reading the selected text from another application requires Accessibility, which is a serious permission. Offer the clipboard route first, which needs nothing, and request Accessibility only when a preset that needs the selection is actually used, naming the preset. ## M5 · Attachments and cost You can send a file and you always know what a conversation has cost. ### Steps 1. Handle attachments per capability Check the model's capability first and fail clearly rather than sending something it will reject. Files: `Attachments.swift` 2. Record tokens and cost per message Including context tokens, which is the number that explains the bill. Files: `Cost.swift` 3. Reconcile against the provider Compare your total with the provider's dashboard for the same window. Investigate a gap. Files: `Cost.swift` ### Done when - [ ] An image reaches a capable model and errors clearly on an incapable one - [ ] Per-turn token count includes context - [ ] Conversation cost matches the provider dashboard closely - [ ] A threshold crossing produces a warning ### Watch out - Sending a long document as context on every turn re-sends it with each message, so a twenty turn conversation pays for the document twenty times. Show the per-turn token count including context, because that number is what explains a bill that looks impossible. ## M6 · Signing, distribution and what leaves the machine (production only) Others can install it and know exactly where their conversations go. ### Steps 1. Sign and notarize Hardened runtime, Developer ID, notarytool, stapler. ```sh xcrun notarytool submit Chat.zip --keychain-profile AC --wait ``` 2. Show the active provider always In the window, not buried in settings. It is the most important fact on screen. Files: `ProviderBadge.swift` 3. Document the local-only path A configuration that sends nothing, verified with networking off. Files: `README.md` ### Done when - [ ] spctl reports the notarized build accepted - [ ] The active provider is visible in the window - [ ] A local-only configuration works with networking disabled - [ ] An appcast update installs and relaunches ### Watch out - A chat client's whole risk surface is that message content goes to a provider, and users assume a native application is more private than a web one. Say exactly which provider receives what, and make it visible in the window rather than only in settings.
# Operations · BoltAI ## Backup The database contains every conversation, which for many people includes work they would not want disclosed. Back it up encrypted, and consider a retention policy rather than keeping everything forever. ## Restore Restore the database and the keychain entries. Without the keys the history is readable and the application cannot send anything, which is a reasonable degraded state. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing runs as a service. Track streaming smoothness and search latency against the ten thousand message fixture across releases, since both degrade quietly as features accumulate. ## Incident checklist A provider changing its streaming format or pricing is the recurring maintenance. Because both are inside adapters, the blast radius is one file, which is the reason for the abstraction. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Streaming stays smooth with the window responsive on a long response - [ ] Search latency on the ten thousand message fixture has not regressed - [ ] No key material appears in the database - [ ] The active provider is visible in the window - [ ] A local-only configuration is verified to send nothing ## Launch constraint Do not market omitted BoltAI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Conversations and messages. Contains everything you have discussed, so treat backups accordingly. DB_PATH=~/Library/Application Support/Chat/chat.db # Required. Which provider is active on launch. Local means nothing leaves the machine. DEFAULT_PROVIDER=local # Required. How often buffered tokens are flushed to the view. Frame rate, not token rate. STREAM_FLUSH_HZ=60 # Required. Messages loaded per page in a conversation. HISTORY_PAGE_SIZE=80 # Required. Conversation cost at which a warning appears. COST_WARN_USD=1.50 # Optional. Product phase only. SPARKLE_FEED_URL=https://example.com/appcast.xml
$ choose a build depth, inspect the files, then open the complete pack in your agent
People do not pay for BoltAI because it is the only app capable of putting a prompt box beside an API. Many native and Electron clients already do that. They pay because BoltAI is a fast, coherent Mac workspace where long histories, streaming responses, keyboard navigation, global panels, dictation, workflows, files, and tools behave like one product. Achieving that requires continuous profiling, AppKit-level engineering, product taste, and repair of real-world edge cases. AI can generate a large SwiftUI codebase quickly; it cannot one-shot the accumulated judgment and maintenance required to make that code feel native and remain performant.
xAppKit-backed performance and responsiveness across large workspaces and conversation histories
xcarefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior
xAI Workflows that capture context from other applications and return formatted results where the user is working
xInstant Chat and Instant Dictation as reliable system-wide workflows rather than ordinary in-app screens
xmaintained provider compatibility, Cloud Sync, mobile continuity, imports, backup recovery, and protected credential portability
Don't feel like building it? These folks already made it free.
all 8 free alternatives to BoltAI →· no votes, no pay-to-list · just what's real
BoltAI pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| evaluation | $0/user | — | Evaluation access; exact duration and numeric usage cap are not published. |
| essential | custom | — | 1 seat; 1 desktop device; 512 MB cloud storage; 1 year of updates, cloud access, and mobile access. |
| pro | custom | — | 2 desktop devices + 1 mobile device; 1 GB cloud storage; 1 year of updates, cloud access, and mobile access. |
| team perpetual | custom | — | Minimum 5 seats; 1 device/seat; 10 GB shared cloud storage; 1 year of updates and cloud access. |
free tierno permanent free tier; evaluation mode exists, but its numeric cap is not published
billingone-time perpetual licenses; 1 year of updates/cloud included; optional annual renewal, no monthly subscription
hidden costsModel-provider API usage is BYOK and billed separately. Personal renewal defaults to 60% of the original pre-discount license price; Team requires 5 seats. After included cloud access ends, data becomes read-only and may be deleted after 24 months without renewal.
verified 2026-08-12 · source ↗
Vibecode BoltAI
Kinda. The core of BoltAI is buildable in a weekend with the prompt on this page, but there are real gaps: AppKit-backed performance and responsiveness across large workspaces and conversation histories, carefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior. Read the honest list above before committing.
How much does BoltAI cost?
BoltAI's pricing is usage-based or varies by plan · List price is 79 USD; a temporary sale price of 55 USD was shown when checked..
What do I lose by replacing BoltAI?
Honestly: AppKit-backed performance and responsiveness across large workspaces and conversation histories; carefully tuned keyboard navigation, focus, menus, panels, windows, drag and drop, and other native Mac behavior; AI Workflows that capture context from other applications and return formatted results where the user is working; Instant Chat and Instant Dictation as reliable system-wide workflows rather than ordinary in-app screens; maintained provider compatibility, Cloud Sync, mobile continuity, imports, backup recovery, and protected credential portability. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to BoltAI?
Yes: AnythingLLM (A private AI desk for documents and agents; install it, then choose local models or somebody else's meter.) Cherry Studio (A desktop workspace for many models, agents, files, tools, and an impressive number of toggles.) Cherry Studio (A desktop model switchboard with assistants, files, agents and MCP; bring keys or make your laptop sweat.) All 8 curated free alternatives are at vibecodeit.com/boltai/alternatives. The prompt is for when you want it exactly your way.