A macOS command palette that puts five AI models in a debate and returns one answer.
ResolveDemo.mp4
Select text in any app, press Cmd + Shift + A, and five models answer independently.
Ask a single model a hard question and you get an answer with no error bars. You cannot tell a confident correct answer from a confident wrong one.
Resolve sends your question to five models at once, groups their answers by the position each one took, and has an arbiter synthesise a single response with the disagreements shown rather than hidden. If they disagree, you can run a debate round: each model sees the opposing positions and decides whether to hold or change its answer.
It lives in a floating panel summoned with a global hotkey, so it works over whatever you are already reading.
flowchart TD
Q[Question or selected text] --> L{Multiple choice?}
L -- yes --> LB[Labeller<br/>normalises options]
L -- no --> FO[Fan-out]
LB --> FO
FO --> A1[Mistral]
FO --> A2[DeepSeek]
FO --> A3[Gemini]
FO --> A4[Anthropic]
FO --> A5[OpenAI]
A1 --> CL[Classifier<br/>groups answers by stance]
A2 --> CL
A3 --> CL
A4 --> CL
A5 --> CL
CL --> AR[Arbiter<br/>synthesises]
AR --> OUT[Answer + open disagreements]
OUT --> R{Resolve round?}
R -- yes --> RD[Advocates see opposing<br/>stances and reconsider]
RD --> CL
R -- no --> DONE[Done]
Fan-out: The prompt goes to every enabled advocate concurrently. Each returns a position and its reasoning.
Classifier: A separate model pass groups the five answers by the position they took, so the UI can tell consensus from a genuine split and colour the cards accordingly. Counting identical strings would not catch two models agreeing in different words. Advocate responses are stripped of provider identity before this pass.
Arbiter: Synthesises a single answer and states where the advocates diverged. It sees the same anonymised responses as the classifier, so it cannot prefer its own provider's answer. Tone is configurable between balanced and decisive.
Resolve rounds: Each advocate is re-prompted with its own prior position plus the opposing stances, then decides whether to hold or revise. Classification and arbitration re-run. Every round is stored as a snapshot, so you can step back and forward through the debate with Cmd + [ and Cmd + ]. Three rounds per question by default, configurable in settings.
ResolvingDemo.mp4
The clip in the header asks whether woolly mammoths were still alive when the Egyptian pyramids were built. Gemini says they were extinct by then. The other advocates disagree.
Cmd + Shift + R runs a debate round. Each advocate sees the opposing positions and decides whether to hold or revise. Gemini revises, and the arbiter converges on the Wrangel Island population surviving to roughly 1650 BCE, which overlaps pyramid construction.
- Five advocates: OpenAI, Anthropic, Gemini, DeepSeek and Mistral, individually toggleable
- Stance classification, so the UI shows agreement and disagreement rather than five loose answers
- Iterative resolve rounds with per-round snapshots and history navigation
- Multiple-choice handling through a labelling pass that normalises options before fan-out
- Advocate drawer for the full reasoning behind any card (
Cmd + 1toCmd + 5) - Multiple panel instances, each with its own conversation
- Persisted conversations with search, rename and delete
- Markdown export of any debate
- Configurable arbiter tone, panel size, translucency, stance colours and theme
Resolve is keyboard-first. The full list is in the app under Cmd + H.
| Shortcut | Action |
|---|---|
Cmd + ; |
Toggle the palette from anywhere |
Cmd + Shift + A |
Ask Resolve with text selected in another app |
Cmd + N |
New chat |
Cmd + Shift + R |
Run a resolve round |
Cmd + [ / Cmd + ] |
Previous / next round |
Cmd + 1 to Cmd + 5 |
Open an advocate's reasoning |
Cmd + Shift + N |
New panel instance |
Cmd + F |
Search past chats |
Cmd + Shift + C |
Copy the arbiter summary |
Cmd + Shift + E |
Export the debate as markdown |
App. SwiftUI and AppKit, macOS 15.5 and above. Runs as LSUIElement, so it is a floating utility panel rather than a Dock application. Panel lifecycle is owned by CommandPanelManager and commandPanelController.
Backend. Node and Express with Postgres, in a separate repository. Handles fan-out, classification, arbitration and persistence.
Auth. Clerk OAuth with PKCE through ASWebAuthenticationSession. Tokens live in the Keychain under the resolve.auth service. The callback scheme is resolve://auth-callback.
Persistence. Conversations, messages, runs, advocate outputs, arbiter outputs, classifier outputs and label outputs. A run is one initial pass or one resolve round. Conversation detail returns the latest completed run, so a failed or in-flight round never hides the last good result. resolve_count is maintained by a database trigger on run completion rather than in application code, which keeps it correct if a request dies midway.
Resolve needs the backend running first.
cd resolve-backend
npm install
npm run devCheck it is up:
curl http://localhost:3000/health
# {"status":"ok","time":"..."}Then open resolve.xcodeproj, select the resolve target, and press Cmd + R.
The backend reads a .env in its own repository root:
PORT=3000
DATABASE_URL=
CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
ADVOCATE_ONE= # OpenAI
ADVOCATE_TWO= # Anthropic
ADVOCATE_THREE= # Gemini
ADVOCATE_FOUR= # DeepSeek
ADVOCATE_FIVE= # Mistral
LABELLER= # OpenAI, multiple-choice normalisation
CLASSIFIER= # OpenAI, stance grouping
ARBITER= # OpenAI, synthesis
NAMER= # OpenAI, automatic conversation titles (optional)Restart the backend after any .env change.
| Method | Route | Purpose |
|---|---|---|
GET |
/health |
Database connectivity |
GET |
/me |
Verify Clerk auth |
POST |
/conversations |
Create a conversation |
GET |
/conversations |
List conversations |
GET |
/conversations/:id |
Conversation detail and messages |
PATCH |
/conversations/:id |
Rename |
DELETE |
/conversations/:id |
Delete with child rows |
POST |
/conversations/:id/messages |
Add a message and run the initial debate |
POST |
/conversations/:id/messages/:messageId/resolve |
Run a resolve round |
GET |
/conversations/:id/runs/:runId |
Full run payload |
GET |
/conversations/:id/messages/:messageId/runs |
Rounds for a message |
A classifier pass instead of string comparison. Two models can give the same answer in wording that shares almost no tokens. Grouping by classified stance is what makes "four agree, one dissents" something the UI can show.
Anonymised advocate responses. The classifier and the arbiter never see which provider wrote which answer. The arbiter is itself a model, and a model shown a labelled response from its own provider has a measurable preference for it. Stripping the labels removes that.
A floating panel rather than a window. Resolve runs as an LSUIElement panel so it can sit above other applications, including full-screen ones. You keep the debate visible while working in whatever raised the question, and a single key gets it out of the way.
User-triggered resolve rounds. Rounds run when you ask for one rather than automatically until convergence. You see the state after each round and decide whether another is worth it, and often it is not. Watching advocates flip each other's positions is also more informative than being handed the end state.
- Advocates sometimes talk each other out of a correct answer during a resolve round.
- Each question costs roughly five model calls plus two more for classification and arbitration, and every resolve round repeats that.
- No benefit on questions with no verifiable answer.
- The backend URL is currently hardcoded to
http://localhost:3000inBackendAPIClient.swiftandBackendAIClient.swift.