NOTE: This is very much a work in progress. Use at your own peril
One of the best things you can do these days with LLMs is bridge that skill gap you always had. Learn, experiment, explore, become better. While, straight up asking a model something is good, I found that I tend to get sidetracked from time to ime. I wanted to have the ability to ask a LLM questions about a part of it's answer without having to scroll all the way down and lose my flow. So, because of this Kiro and I built Marginalia - get your knowledge on the margins of an answer!
Marginalia is a web-based LLM explainer tool that renders structured explanations as documents and supports inline margin notes for follow-up questions. Select any passage in the explanation, ask a side question, and get an answer in a margin note, all while the LLM maintains full context of every thread.
This all started with this Excalidraw drawing I made:
And from there, with some Strands and a whole lot of TypeScript, it sort of became this freakin' thing! 🥳
- Node.js 18+
- AWS credentials configured (Bedrock access required)
- A Bedrock-enabled model (defaults to
qwen.qwen3-vl-235b-a22b)
npm installnpm startOpens on http://localhost:3000.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
BEDROCK_MODEL_ID |
qwen.qwen3-vl-235b-a22b |
Bedrock model to use |
BEDROCK_MAX_TOKENS |
8192 |
Max output tokens per response. Raise for longer answers; some models (e.g. Anthropic Claude) error out if left at their low Bedrock default |
MARGINALIA_DATA_DIR |
~/.config/marginalia/ |
Override data/config directory |
If you want to start it with a different model you can do:
BEDROCK_MODEL_ID="moonshotai.kimi-k2.5" npm startA justfile is included for convenience. It lets you launch the
server with a specific model and pick which AWS profile to authenticate with
(handy if you keep your Bedrock access on a personal/named profile):
just run # default model + default AWS profile
just run "moonshotai.kimi-k2.5" # pick a model
just run "moonshotai.kimi-k2.5" personal # pick a model + AWS profileThe selected profile is exported as AWS_PROFILE, which the Strands
BedrockModel picks up via the standard AWS SDK credential chain — no code
changes needed. You can also override the defaults globally:
just model="amazon.nova-pro-v1:0" profile="personal" runOther recipes: just install, just dev (auto-reload), just build,
just test. Run just on its own to list them all.
To raise the output length cap (e.g. for very long answers, or to avoid the "maximum token limit" error some Claude models raise at their low default):
just maxtokens="16384" model="<your-claude-model-id>" profile="personal" runnpm run devUses tsx watch for auto-reload on file changes.
npm testRuns 260 tests (property-based + unit) (thank you Kiro ❤️) via Vitest across models, context assembly, conversation ops, validation, agent, retry, SSE, routes, layout, markdown rendering, MCP config management, data directory resolution, system prompt persistence, and conversation export.
- Type a question in the input bar at the top
- The LLM streams a markdown-rendered explanation into the main panel
- Select any text in the explanation > a popover appears > ask a side question
- The answer appears as a margin note anchored to your selection with some SVG lines
- Each margin note supports follow-up questions within its own thread
- Continue the main conversation below — your follow-up questions appear as styled cards (blue left border, light background) above each response, and the LLM sees all margin note context
- Export your conversation via the export button (top bar) — choose Markdown, HTML, or JSON. The HTML export is a self-contained file with the two-column layout, color-coded highlights, and clickable numbered badges that link inline annotations to their margin notes.
- TypeScript / Express backend
- Strands Agents SDK for Bedrock integration
- SSE streaming for real-time responses
- Vanilla HTML/JS frontend (no build step)
- marked.js, highlight.js, tippy.js (this one caused me troubl), DOMPurify via CDN
- CSS Custom Highlight API for text anchoring
- MCP tool integration via settings UI with persistent config
- Persistent system prompt (survives restarts, stored in
system-prompt.md) - Conversation export: Markdown (side threads as blockquotes), self-contained HTML (two-column layout, numbered clickable badges), raw JSON
- Graceful MCP server shutdown on SIGINT/SIGTERM
By default, Marginalia stores all data and config in ~/.config/marginalia/:
~/.config/marginalia/
├── chats/ # Conversation JSON files
│ └── {uuid}.json
├── mcp.json # MCP server configurations
└── system-prompt.md # Custom system prompt (optional)
Override with MARGINALIA_DATA_DIR:
MARGINALIA_DATA_DIR=./local-data npm startMCP servers can be configured through the settings UI or by editing mcp.json in your data directory (~/.config/marginalia/mcp.json by default). The file follows the VS Code/Cursor/Kiro convention:
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "@some/mcp-server"],
"env": { "API_KEY": "..." },
"enabled": true
}
}
}Servers can be enabled/disabled individually without removing them. Config is loaded on startup and saved automatically on every change.
- Add support to other model providers
- Improve stability (yeah)
- The SVG lines get messed all the time up until you scroll
- Dark mode
- Make the SVG lines smoother

