Think Tank
Stop arguing with one AI. Convene a roundtable.
Think Tank
Stop arguing with one AI. Convene a roundtable.
Think Tank turns frontier LLMs into a panel of advisors that debate, refine, and decide together. Pick participants from 11 providers (OpenAI, Anthropic, Google, DeepSeek, Mistral, xAI, Groq, Together AI, OpenRouter, Fireworks, Cohere), assign each a personality, drop a topic into the room, and watch them think out loud. Inject your own messages mid-discussion. Call a vote when they go in circles. Run multiple debates in parallel tabs — all in a plain browser tab, no install.
This document is the practical "how do I build/run/test it" tour. For the canonical architecture record (what the system IS / is NOT, the Laws, verified test state), see docs/BIBLE.md — this README does not duplicate that; it links to it.
Table of contents
- What it is
- Documentation canon (Codex)
- Solution & project layout
- Architecture
- ThinkTank.Blazor — the host
- ThinkTank.Core — services & models
- ThinkTank.Shared — pages & components
- Feature tour
- Configuration
- Voting
- Data persistence
- Getting started
- Testing
- Cypress e2e coverage
- Supported providers
- Other files at the repo root
- Known gaps / stale notes
What it is
Think Tank is an ASP.NET Core Blazor Server web app (.NET 10, SignalR transport, no WASM, no native shell). It is not a single-model chat client — the unit of work is a panel, not a 1:1 conversation. It is not a provider SDK — every model call is routed through MindAttic.Legion, which owns the provider catalog, HTTP transport, retries, and voting logic; Think Tank only assembles prompts, history, and personas. It is not a credential store — cloud-resolved API keys live in a runtime-only side map via MindAttic.Vault and are never written to disk. It is not multi-user — state is shared globally on a single host with no accounts.
It used to be a .NET MAUI desktop app; the MAUI shell was fully retired in favor of Blazor Server
(see AMENDMENTS.md#TT-A1). It was also originally called LLMThinkTank/"Arena" before being
renamed (AMENDMENTS.md#TT-A2).
Documentation canon (Codex)
This repo has adopted the MindAttic Codex documentation standard: each fact lives in exactly one layer, linked by stable ID rather than line number.
| Layer | File | Purpose |
|---|---|---|
| L0 | docs/BIBLE.md | What the system IS / is NOT, architecture, the Laws (TT-LAW-n), verified test state, glossary. Anchors {#TT-§N} / {#TT-LAW-n}. |
| L1 | docs/AMENDMENTS.md | Append-only change log (TT-A<n>). An amendment wins over the Bible; entries are never rewritten, only superseded. |
| L2 | docs/USER_STORIES.md | Stories TT-US-<Epic><n>; every ✅ cites the NUnit test that proves it. |
| rfc | docs/rfc/ | Design notes for work not yet graduated into L0/L2. Currently: 0001-auto-vote-after-n-rounds.md. |
| generated | docs/BIBLE.digest.md | Produced by tools/codex.ps1 digest. Never hand-edit. |
Status legend used across the canon: ✅ done (test/build-verified) · 🟡 partial ·
⬜ planned · 🗑️ cut · living.
Regenerate/validate the canon with:
powershell -File tools/codex.ps1 digest # regenerate docs/BIBLE.digest.md
powershell -File tools/codex.ps1 doctor # validate IDs, links, cited tests/paths, digest freshness
Solution & project layout
ThinkTank.slnx (the newer XML .slnx solution format) lists exactly four projects, in this order:
ThinkTank/
├── ThinkTank.slnx
├── ThinkTank.Core/ Services + models class library (no UI)
│ ├── Models/ ParticipantTemplate, ChatParticipant, ChatConversation, SharedTurn, ...
│ └── Services/ ThinkTankService, VotingService, SettingsService, AppearanceService, ...
├── ThinkTank.Shared/ Razor class library — every page and component + wwwroot assets
│ ├── Components/Layout/ MainLayout, NavMenu
│ ├── Components/Pages/ Home, Chat, Settings, SettingsAppearance, NotFound
│ ├── Components/Shared/ ConfirmationDialog
│ └── wwwroot/ app.css, theme.js, vendored Bootstrap CSS
├── ThinkTank.Blazor/ ASP.NET Core host — Program.cs is the single DI composition root
├── ThinkTank.UnitTests/ NUnit 4 + bUnit — 21 test files
├── cypress/e2e/ Cypress specs: navigation, settings, chat, vote-dialog
├── cypress.config.js
├── docs/ Codex canon (BIBLE.md, AMENDMENTS.md, USER_STORIES.md, digest, rfc/)
├── index.htm Static MindAttic.com landing/marketing page (see below — currently stale)
├── package.json Cypress + landing-page build/deploy scripts
├── scripts/cli/ Landing-page build/deploy tooling (partially untracked, see note below)
└── tools/codex.ps1 Codex digest/doctor tooling
Architecture
Browser
Blazor Server components (Home / Chat / Settings / ...)
| SignalR (interactive server)
v
ThinkTank.Blazor (ASP.NET Core host)
Program.cs wires DI: Legion, Vault, services, voting
|
+--------------------+--------------------+
v v v
ThinkTank.Shared ThinkTank.Core ThinkTank.Blazor
(Razor lib + (services + models) (host)
wwwroot) |
v
MindAttic.Legion
LegionClient + LLMVotingService +
LlmProviderCatalog + PersonaStore
|
+--------------------+--------------------+
v v v
OpenAI Anthropic ... 9 more
(ChatGPT) (Claude) providers
All services are registered as singletons in ThinkTank.Blazor/Program.cs — one shared engine
and DI graph across every Blazor circuit (per the org-wide HOUSE-LAW-6). Full architecture
narrative, the domain model ("nouns"), the service list ("verbs"), and the five project Laws
(TT-LAW-1..TT-LAW-6) live in docs/BIBLE.md §4 and
§5 — summarized in the table below.
| Project | SDK / target | Role |
|---|---|---|
ThinkTank.Core |
classlib, net10.0 | Services + models; references MindAttic.Legion (22.0.0) + MindAttic.Vault (1.0.0). No UI. |
ThinkTank.Shared |
Razor SDK, net10.0 | Razor component library + wwwroot (CSS, 18 themes, theme.js, vendored Bootstrap). |
ThinkTank.Blazor |
Web SDK, net10.0 | ASP.NET Core host. Program.cs is the single composition root. |
ThinkTank.UnitTests |
NUnit 4 + bUnit, net10.0 | Service, model, component, security, and psychometric tests. |
None of the four .csproj files carries a <Version> tag — Think Tank is run via dotnet run,
not packaged/published as a versioned artifact (unlike the NuGet-consumed MindAttic.Legion
22.0.0 / MindAttic.Vault 1.0.0 it depends on).
ThinkTank.Blazor — the host
ThinkTank.Blazor.csproj targets Microsoft.NET.Sdk.Web / net10.0 and references
ThinkTank.Core + ThinkTank.Shared. Its only files are Program.cs, Components/App.razor,
Components/Routes.razor, Components/_Imports.razor, and Properties/launchSettings.json.
Program.cs is the entire composition root:
builder.Configuration.AddMindAtticVaultFiles()— layers configappsettings.json→ Vault files (%APPDATA%\MindAttic\LLM\providers.json) → environment variables (Azure App Service / Key Vault in production).AddRazorComponents().AddInteractiveServerComponents()— confirms Blazor Server render mode (not WebAssembly, not Hybrid).builder.Services.AddLegionClient()— registersMindAttic.Legion; Legion owns its ownIHttpClientFactoryinternally.- A singleton factory builds
SettingsService, seedsProviderDefaultsfrom configuration (mapping e.g.claude→"anthropic",gemini→"google", else"bearer"), then layers the Vault overlay viasettings.OverlayFromConfiguration(config). - Registers
ThinkTankSettingsService,ChatLogService,AppearanceService,ChatConversationsService,HumanNameService,NameGeneratorService,PsychometricProfileService,ThinkTankService,AddLLMVoting(...)(withVotingConfiguration { AllowedProviderIds = LlmProviderCatalog.DefaultIds }),VotingService— all as singletons. - Pipeline: exception handler / HSTS / HTTPS redirect only outside Development,
UseStaticFiles,UseAntiforgery,MapRazorComponents<App>().AddInteractiveServerRenderMode() .AddAdditionalAssemblies(typeof(ThinkTank.Shared.Components.Pages.Home).Assembly).
ThinkTank.Core — services & models
Class library, no UI. Two folders:
Models/
| File | Types |
|---|---|
ChatModels.cs |
ParticipantTemplate (seat definition: ProviderId, DisplayName, PersonalityMarkdown, optional AuthOverrideJson, IsDefault, optional PersonaId), ChatParticipant (an instantiated seat with EffectivePersonaId), ChatConversation (one tab: ChatId, Title, Participants, Topic, per-tab MaxTokens/MaxRounds/ResponseLength, Messages, StatusEvents, Diagnostics) |
LlmModels.cs |
SharedTurn, ConversationMessage, LlmModel |
PersistenceModels.cs |
PersistedConversation, PersistedParticipant, PersistedMessage, PersistedTurn, PersistedStatusEvent |
ProviderAuthConfig.cs |
(ProviderId, Json) auth blob |
ChatLogModels.cs, AppearanceMode.cs, ResponseLengthPreset.cs |
Log entries, theme mode, response-length presets |
Services/
| Service | Responsibility |
|---|---|
ThinkTankService |
Orchestration engine — builds per-participant history, applies personality markdown as system prompt, trims to MaxContextTurns, sanitizes self-reference prefixes (e.g. strips leading [Claude]:), emits redacted Diagnostics events. Every model call goes through LegionClient. |
VotingService |
Maps ChatParticipant[] → Legion VoterProfile[] (MapToVoterProfiles), delegates to LlmVotingService.VoteWithProfilesAsync. |
SettingsService + SettingsServiceVaultOverlay |
Persists to %LOCALAPPDATA%\MindAttic\ThinkTank\Settings.json; GetKeyForProvider implements the fixed credential precedence; BuildAuthJson. |
PsychometricProfileService + PsychometricNarrator |
Resolve a participant's Legion persona and render OCEAN/HEXACO/MBTI/Enneagram/DISC as a behavioral brief appended to the system prompt. |
AppearanceService |
18-theme + control-height/gutter/border-radius state, applied via JS interop, persisted through SettingsService. |
ChatConversationsService |
Tab lifecycle over ObservableCollection<ChatConversation> — NewId(), CreateConversation, SetActive, CloseConversation. |
ChatLogService |
In-memory event log with a Changed event. |
HumanNameService / NameGeneratorService |
Random and AI-generated participant names (GenerateFirstName, falls back to "Alex" on empty/garbage output). |
ThinkTank.Shared — pages & components
Razor class library (Microsoft.NET.Sdk.Razor, EnableDefaultCssItems=false), references
ThinkTank.Core, pulls in Microsoft.AspNetCore.Components.Web. Styling is hand-rolled
wwwroot/app.css plus vendored Bootstrap CSS (no component library like MudBlazor/Radzen);
wwwroot/theme.js applies the 18-theme CSS variables via JS interop.
| Page/component | Route | What it does |
|---|---|---|
Home.razor |
/ |
Trivial landing page — links to "Open Think Tank" (/thinktank) and "Settings" (/settings). |
Chat.razor |
/thinktank |
The centerpiece (~1,800 lines). See feature tour below. |
Settings.razor |
/settings |
Three tabs: Personas (edit ParticipantTemplates — name, provider, model override, custom auth JSON), Defaults (global max tokens/rounds/response-length, Claude-fallback toggle, a notice that keys are managed entirely via MindAttic Vault — no in-app key entry), Appearance (delegates to SettingsAppearance). |
SettingsAppearance.razor |
(embedded) | 18 themes + control-height/gutter/border-radius sliders. |
NotFound.razor |
(fallback) | 404 page. |
Shared/ConfirmationDialog.razor |
(embedded) | Reusable confirm/cancel dialog. |
Layout/MainLayout.razor, Layout/NavMenu.razor |
(layout) | App shell + nav. |
Feature tour
Everything below is implemented in Chat.razor's markup + @code block:
- 11 providers, routed exclusively through
MindAttic.Legion— Think Tank never calls a provider directly (TT-LAW-1). - Conversation tabs — a tab strip supports multiple independent debates in parallel, with a right-click context menu for rename/close; tabs persist across restarts.
- Personality system — each seat is a markdown personality template; personas can instead be picked from the Legion persona library ("Add persona from Legion library" dialog, searchable by name/personality text) with a psychometric profile rendered into the system prompt.
- Setup panel — topic textarea, a "Random topic" button (calls an LLM to generate one), participant pill grid for template selection, Start button (requires ≥2 participants).
- Live round loop —
StartActive()builds shared history from persisted messages, supports mid-session resume (detects an incomplete last round), loops up toMaxRounds, callingThinkTankService.CallProvider(...)once per participant per round with the personality markdown- vote-request instruction, auth override, topic, shared history, response length, and persona id.
- User injection — pause the conversation, type a message in the bottom chat bar; the round loop resumes automatically afterward.
- Vote-driven decisions — a Call Vote button opens a dialog with three vote types
(Consensus Yes/No, Free-form, Direction with comma-separated custom options); participants can
also self-trigger a vote by emitting
[REQUEST_VOTE: question], detected and stripped from their visible response. - Live status UI — member sidebar shows who's "speaking" with animated typing dots, per-conversation Max Tokens/Max Rounds/Response Length overrides, a "Claude only" fallback toggle, Export transcript, Pause/Resume, and a collapsible Status Log with three tabs (Perspective / Context / Diagnostics).
- Chat title generation — after round 1, each participant is asked for a title suggestion in the background, and one participant picks the best.
- 18 themes — dark, light, spring, summer, autumn, winter, matrix, ice, sunset, neon, dracula, solarized, midnight, aurora, ember, ocean, forest, mono.
- Perspective tracking — per-participant markdown per conversation, visible in the status panel.
- Fault surfacing —
ObserveFaultwires fire-and-forget background tasks (title generation, auto-vote) so errors land in the Diagnostics panel instead of vanishing silently.
Configuration
Provider auth
{ "type": "bearer", "apiKey": "sk-...", "model": "gpt-4o", "maxTokens": 2048 }
type values: "bearer" (OpenAI-compatible), "anthropic", "google".
Cloud credentials
Keys resolve from %APPDATA%\MindAttic\LLM\providers.json (via AddMindAtticVaultFiles) or
MindAttic:Vault:LLM:<providerId>:apiKey in the environment / Azure App Service Application
Settings. Cloud-resolved keys are held in a runtime-only side map and are never written back to
Settings.json (TT-LAW-2). Resolution precedence (TT-LAW-3): explicit per-call override →
on-disk apiKey in Settings.json → Vault/cloud runtime override.
Appearance
Settings → Appearance: theme (18 options), control height (28–60px), gutter (0–30px), border radius (0–24px). All values are clamped.
Voting
The Call Vote button polls every participant and injects the aggregated result into shared
history as a synthetic turn (TT-LAW-4):
[VOTE] Question: <question>
Decision: <consensus> (<percentage> agreement)
Summary: <narrativeSummary>
Vote types: Consensus (Yes/No), Free-form (open-ended), Direction (custom options).
Implemented in VotingService via MindAttic.Legion.LLMVotingService.
Auto-vote: every participant's system prompt ends with an instruction permitting them to emit
[REQUEST_VOTE: question] to trigger an immediate vote; the marker is stripped from the visible
response. See Chat.razor → VoteRequestInstruction. Auto-vote after N rounds of no convergence
(as opposed to a self-triggered marker) is not yet implemented — see
docs/rfc/0001-auto-vote-after-n-rounds.md.
Data persistence
Persistence must fully reconstruct a conversation after restart (TT-LAW-5):
%LOCALAPPDATA%\MindAttic\ThinkTank\
├── Settings.json All app settings (provider auth, templates, conversations, appearance)
├── Personalities/
│ └── {templateId}.md
└── Conversations/
└── {chatId}/
├── chat.json Append-only event log (chat-start, turn, user)
└── {participantId}.md
Getting started
dotnet restore
dotnet run --project ThinkTank.Blazor
# -> https://localhost:5001
# First launch: Settings > Providers -> enter API key(s), select models
# Then: Conversations -> enter topic, select participants, click Start
Testing
ThinkTank.UnitTests uses NUnit 4.4.0 + bUnit 1.31.3 (component testing) on net10.0, with
Microsoft.Extensions.Caching.Memory pinned to 10.0.5 to override a vulnerable preview transitive
dependency (NU1903 / GHSA-qj66-m88j-hmgj).
dotnet test ThinkTank.UnitTests/ThinkTank.UnitTests.csproj
As of the last BIBLE sync (2026-06-07): 293 passed, 0 failed, 0 skipped (~2s). See docs/BIBLE.md §6 for exactly what is and isn't test-proven.
21 test files, roughly grouped:
| Area | Files |
|---|---|
| Services | AppearanceServiceTests, ChatConversationsServiceTests, ChatLogServiceTests, HumanNameServiceTests, NameGeneratorServiceTests, SettingsServiceTests (largest, 40 [Test]s), SettingsServiceVaultOverlayTests, ThinkTankServiceTests, VotingServiceTests |
| Models / parsing | ModelTests, ProviderAuthConfigParsingTests, VoteMarkerTests |
| Persistence | ChatStorageTests |
| Psychometrics | PsychometricsTests |
| bUnit components | ConfirmationDialogComponentTests, HomePageComponentTests, NavMenuComponentTests, NotFoundPageComponentTests, SettingsAppearanceComponentTests |
| Security | Security/NoSecretsCommittedTests (guard test currently disabled — see AMENDMENTS.md#TT-A4) |
| Setup | TestAssemblySetup |
Cypress e2e coverage
# Blazor app must be running on http://localhost:5100
dotnet run --project ThinkTank.Blazor --urls http://localhost:5100
npm install
npx cypress run # headless
npx cypress open # interactive
cypress.config.js: baseUrl http://localhost:5100 (override with CYPRESS_BASE_URL),
specPattern cypress/e2e/**/*.cy.js, no support file, screenshots on failure, videos off,
15s command timeout, 60s page-load/response timeout, 1600×900 viewport, chromeWebSecurity: false.
Four specs in cypress/e2e/:
| Spec | Coverage |
|---|---|
navigation.cy.js |
Basic routing between pages. |
settings.cy.js |
Settings page tabs/behavior. |
chat.cy.js |
Tab-strip rendering, "+ New" tab creation, right-click rename/close context menu. Deliberately does not intercept live LLM dispatch (that happens server-side via Legion) — left to the C# unit suite. |
vote-dialog.cy.js |
Setup-panel participant pills + Start button, pill aria-pressed toggling, opening the Call Vote dialog (asserts all 3 vote-type radios present, default text matches Consensus), switching to Direction reveals the custom-options input, Cancel closes the dialog. Deliberately does not submit an actual vote (would trigger real LLM calls). |
Both chat.cy.js and vote-dialog.cy.js use a blazorClick helper that dispatches a raw
MouseEvent at the document level to bypass Cypress actionability checks against Blazor Server's
@onclick-bound elements, and wait ~2500ms after cy.visit for the SignalR circuit to hydrate.
These flows are UI-only — not yet proven by the unit suite (see docs/USER_STORIES.md priority backlog: graduating the round loop and vote dialog from Cypress-only to unit-proven is an open item).
Supported providers
All calls route through MindAttic.Legion.LegionClient. Default models:
| Provider | Default model |
|---|---|
| Claude (Anthropic) | claude-sonnet-4-6 |
| ChatGPT (OpenAI) | gpt-4.1-mini |
| Gemini (Google) | gemini-2.5-flash |
| DeepSeek | deepseek-chat |
| Mistral | mistral-large-latest |
| Grok (xAI) | grok-3-mini-fast |
| Groq | llama-3.3-70b-versatile |
| Together AI | meta-llama/Llama-3-70b-chat-hf |
| OpenRouter | meta-llama/llama-3.1-8b-instruct:free |
| Fireworks | accounts/fireworks/models/llama-v3p1-70b-instruct |
| Cohere | command-r-plus |
Other files at the repo root
index.htm— a static, self-contained MindAttic.com marketing/landing page (synced byMindAttic.UiUx/sync/sync-landing-page.ps1tooling — not hand-edited). Embeds a base64 "Outfit" variable font. This file is out of scope for this README to modify; see the stale-content note below.package.json(thinktank-e2e) — Cypress scripts (e2e,e2e:open,e2e:nav) plus landing-pagebuild/deployscripts (node scripts/cli/build-html.js,powershell ... scripts/cli/deploy.ps1). Dependencieshighlight.js/markedsupport that build/deploy tooling, not the app itself.scripts/cli/— onlydeploy.settings.jsonis tracked in git;build-html.jsanddeploy.ps1referenced bypackage.jsonexist locally but are untracked/gitignored.tools/codex.ps1— Codexdigest/doctortooling fordocs/.tools/build-readme.ps1— thin wrapper that regeneratesREADME.htmfrom this file via the sharedcodex-standard/build-readme.ps1engine (same engine every MindAttic repo uses).
Known gaps / stale notes
index.htm's meta description currently reads ".NET 10 MAUI + Blazor desktop app for multi-LLM conversations across 11 providers" — the MAUI shell was retired (docs/AMENDMENTS.md#TT-A1); the product is Blazor Server web-only. This is a landing-page sync lag, not a fact about the current codebase.CLAUDE.mdstill contains a large "Planned Feature: LLMVoting Integration" section describing the original design for voting. That feature has since shipped (Epic C —docs/AMENDMENTS.md#TT-A3); treat that section as historical design notes, not current backlog.- The no-secrets-committed guard test (
Security/NoSecretsCommittedTests.cs) is present but commented out;TT-US-E3is downgraded to 🟡 (docs/AMENDMENTS.md#TT-A4). - Auto-vote after N rounds of stalemate is design-only (RFC 0001), not implemented.