Imported from Dan-works-on-stuff/SnapBoard (
AGENTS.md). Install upstream withnpx skills add Dan-works-on-stuff/SnapBoard. Copyright stays with the author.
AGENTS.md
Purpose
This guide is for autonomous coding agents implementing features and fixes in this repository. Focus is the desktop client in src/; collaboration/backend roadmap details are in Docs/BACKLOG-flyio.md and Docs/Backlog_AI.md.
Current Architecture Status (June 9, 2026)
- ✅ Local drawing app: Fully functional (pen, highlighter, eraser, laser pointer, markdown text, select/move, undo/redo, save/load)
- ✅ Signaling server: Deployed to Fly.io (
fraregion, zero-cost,snapboard-signaling.fly.dev) - ✅ WebRTC client integration: Complete (P2P DataChannels, handshake-tested)
- ✅ Dropbox Cloud Sync: Fully functional (first-time cloud bootstrap, password-protected collaboration rooms, manual cloud save, peer local save redirection, BYOS model)
- ✅ Snappy Chat (AI): Fully functional (Ollama, conversational memory, Quick Ask, toggle-UI, color-sync)
- ✅ Local Vector-AI: Decoupled stable-diffusion.cpp sidecar process (
sd-server), custom high-fidelity boundary-tracing vectorizer, DoodleRedmond LoRA style adapter integration. - ✅ Host Migration: Implemented (Deterministic server-side election + client reset)
- ✅ Real-time drawing sync: Fully functional (Stroke/Move/Delete/Pointer/Text serialization over DataChannel, plus initial
sync_statefor new peers)
Architecture Overview
Application Bootstrap
src/main.cppremains minimal: it createsQApplication, instantiatesDrawingWindow, and handles theWelcomescreen signal chain.- Dynamic Networking: the app starts strictly offline.
NetworkingBuilderand the signaling/WebRTC stack are initialized only when the user enters a cloud collaboration flow.
Cloud Infrastructure (Dropbox)
- BYOS Model:
src/interop/dropbox/DropboxClientinteracts with the user's personal app folder. - Session Lifecycle:
- Host: starts the 120s background
QTimerto push canvas state. - Manual Save: File > Save (Ctrl+S) detects cloud mode and pushes to Dropbox instead of opening a local dialog.
- Peer Save: Attempts to save by a peer (via Ctrl+S or exit confirmation) are intercepted and redirected to local storage with a warning message, writing a clean de-enveloped local JSON.
- Initial Cloud Save: File > Save to Dropbox uploads a local board, prompts for Room ID and room password, and starts a session.
- Join Room: peers connect to an active session by providing the Room ID and Password. The host syncs the current board state to the new peer over WebRTC.
- Emergency Flush:
DrawingWindow::closeEventforces a synchronous final upload if the user is the Host.
- Host: starts the 120s background
AI Assistant (Snappy)
- Ollama Integration: uses
src/interop/ollama/OllamaClienttargetinglocalhost:11434. - UI: a persistent Snappy Chat dock and a Quick Ask menu dialog.
- Features:
- Conversational Chat: Full history-aware interaction inside the dock.
- Quick Ask: Standalone, context-free prompt from the menu (returns response via popup).
- Toggle Visibility: The "Open Snappy Chat" menu action toggles the visibility of the dock widget.
- Markdown rendering, auto-scrolling, and color-sync.
- Visuals: sender name colors in chat automatically synchronize with the active
Canvaspen color (fallback black -> white).
Local Vector-AI (Image Gen)
- Sidecar Process: launches and manages
./build/sd-server(based onstable-diffusion.cppsubmodule) viasrc/interop/sdcpp/SidecarManager. - Model Directory & Size Filter:
- Base checkpoints are loaded from
models/directory. Files must be $>500$ MB to prevent LoRA weights from being misclassified as the main model checkpoint. - LoRAs are loaded from
models/loras/directory (mapped via--lora-model-dirargument).
- Base checkpoints are loaded from
- Asynchronous REST Client:
src/interop/sdcpp/SDClienthandles the async POST requests to/sdapi/v1/txt2imgusing theQNetworkAccessManagerframework. It automatically appends negative prompts, trigger words (doodle, doodleredm), and the LoRA activation tag<lora:DoodleRedmond-Doodle-DoodleRedm:1.0>. - High-Fidelity Tracing:
src/tools/vectorizer/PathTracerthresholds gray intensity at120.- Boundary tracing uses Moore-Neighbor Tracing with Boundary-Only Occupancy Marking (no component flood filling) to prevent skipping interior lines and details connected to the outline.
- Simplification uses RDP (Ramer-Douglas-Peucker) with a tight epsilon of
0.1pixels to preserve the exact geometric shape. - Placed paths are grouped, scaled to fit inside a centered $500 \times 500$ logical canvas bounding box, pushed to the undo history stack, and synchronized over WebRTC.
Secure Credentials (.env)
- Local Dev: use a root
.envfile forDROPBOX_APP_KEY,DROPBOX_APP_SECRET, etc. - CMake: automatically parses
.envand injects keys as compile-time definitions (target_compile_definitions). - CI: injects credentials from GitHub Secrets into the build pipeline.
Collaboration Infrastructure (Deployed)
- Signaling Server (
network/signaling/server.js): Node.js WebSocket relay running on Fly.io (fraregion)- Manages rooms: maps room ID → array of connected peer sockets, enforcing password protection if a password was set by the Host.
- Assigns roles: first peer to join a room becomes "Host", subsequent peers are "Peer"
- Relays SDP/ICE payloads between host and peers for WebRTC handshake
- Broadcasts
host_migrationevents when the host disconnects (peer promotion logic)
- Protocol (WebSocket, JSON-only):
- Client → Server:
{ type: 'join', room: string, peerId: string, password?: string } - Client → Server:
{ type: 'sdp'|'ice', room, source, target, payload } - Server → Client:
{ type: 'role', role: 'Host'|'Peer', hostId } - Server → Client:
{ type: 'sdp'|'ice', source, payload } - Server → Client:
{ type: 'host_migration', newHostId }(when host disconnects)
- Client → Server:
Core Window Construction
DrawingWindowis now an orchestration layer that composes specialized builders in its constructor:MenuBuilder→ File/Edit/Collaboration menusUILayoutBuilder→ tools/colors/thickness docksAIMenuBuilder→ Snappy Chat / Ollama wiringDropboxSessionBuilder→ Dropbox auth, upload/download, cloud session flowNetworkingBuilder→CollaborationManager, DataChannel wiring, and sync setup
Canvas(src/ui/windows/drawing_interface/canvas.h/.cpp) is the drawing state owner. It adheres to the Single Responsibility Principle by delegating logic to modular managers:CanvasRenderer→ Handles high-DPI raster caching and finalpaintEventcomposition.CommandHistory→ Manages the undo/redo stacks and history pruning.TransientPointerManager→ Handles the life-cycle, decay, and updates of non-persistent collaborative pointers.
- The
Canvasclass itself acts as a thin orchestrator, managingDrawablePathandDrawableTextcollections and dispatching events to the activeTool. DrawingWindowstill exposes the public flows used bymain.cppand the builders:startNewProject(),loadProject(filePath),saveProjectAs(),joinRoom(roomId), and the cloud entry points.
Event and Render Flow
- Qt mouse events → translated to logical coordinates by
Canvasvia zoom mapping →Toolinterface methods (src/tools/tool.h). All tools operate in a fixed 1600x900 logical coordinate space. - Render:
Canvas::paintEventdelegates toCanvasRenderer. It uses a hybrid vector-raster approach. Committed paths are drawn to a high-DPIQImagecache, which is then painted scaled to the currentm_zoom. Tool previews viaTool::paintare rendered natively on top after the zoom scaling. - Sharper stroke rendering: the internal cache is intentionally rasterized at a higher resolution via
CanvasRenderer::RenderScale(currently2.0) and then drawn back to the logical canvas size. Keep tool input and mouse mapping in logical coordinates; only the cache rasterization should use the higher backing scale. - Cache Invalidation: any mutation to paths or texts must call
m_renderer.invalidateCache()andupdate(). - Stroke model is
DrawablePath(src/core/drawablepath.h): geometry + color + width. - Text model is
DrawableText(src/core/drawabletext.h): Markdown content + styling + position.
Undo/Redo Architecture
- Undo/redo shortcuts are wired in the menu builder path using
QAction+QKeySequence::Undo/QKeySequence::Redo, connected directly toCanvas::undo()/Canvas::redo(). - Collaborative Sync: Actions performed via undo/redo are broadcast to peers by re-emitting
pathAdded/pathDeleted/pathMovedsignals from within the command lambdas. - ID-Based Tracking: Commands track paths using unique UUIDs rather than list indices to prevent collisions in multi-user sessions.
- A
Deletekey action is also registered and connected toCanvas::deleteSelectedPath(); when the selected stroke is the newest drawn path, Delete behaves like undoing that draw command so the redo stack preserves the path data. Canvasdelegates history management to aCommandHistoryinstance, which replays actions throughCanvasCommand::undo()/CanvasCommand::redo().src/core/canvascommand.h/.cppprovides a genericLambdaCanvasCommandused by canvas mutations.- Implemented undoable canvas actions:
- add stroke (
Canvas::addPath), - delete selected stroke (
Canvas::deleteSelectedPath), - move selected stroke drag (
Canvas::beginMoveAction+ repeatedmoveSelectedPath+endMoveAction).
- add stroke (
- New board reset (
Canvas::clearPaths) is intentionally not undoable: it clears paths and both undo/redo stacks to start from a true blank slate. - Selection drag is grouped as a single undoable move by
SelectToolcallingbeginMoveActionon press andendMoveActionon release.
Persistence Contract (Do Not Break)
- Save:
ProjectSaver::saveToJsoninsrc/core/save/projectsaver.cppwrites{ version: 1, paths, texts }. - Load:
ProjectLoader::loadFromJsoninsrc/core/load/projectloader.cppreconstructs paths and texts, currently ignoring the JSONversionfield. - JSON schema structure:
version: numeric format version (currently1)paths: array of path objects:id: unique UUID stringpoints: array of[x, y]integerscolor: hex string (opaque"#RRGGBB"or alpha"#AARRGGBB")width: numeric stroke width
texts: array of text annotation objects:id: unique UUID stringtext: Markdown content stringx&y: logical coordinatescolor: hex stringsize: integer font size
saveProjectAs()appends.jsonautomatically if the chosen file name does not already end with it.- CLI startup:
SnapBoard <file.json>loads the project immediately and exits without opening the main window.
Build & Deployment (Cross-Platform)
- CMake 3.30.5+ + Qt6 autogen (
AUTOMOC,AUTORCC,AUTOUIC) inCMakeLists.txt. - Required Qt modules:
Core,Gui,Widgets. - Local build (Linux/macOS/Windows):
cmake -S . -B build cmake --build build ./build/SnapBoard # Linux/macOS build\SnapBoard.exe # Windows - Qt path not found? Pass
CMAKE_PREFIX_PATHduring configure. - Windows: post-build DLL/plugin copy is automated in
CMakeLists.txt.
Architecture Documentation (C4 Model)
The system architecture is documented using the C4 model in docs/c4/.
- Level 1: System Context (
context.puml) - High-level view of SnapBoard and its users/external systems. - Level 2: Container (
container.puml) - Technical building blocks (Qt App, Signaling Server, Ollama). - Level 3: Component (
component.puml) - Internal modules of the Desktop Application.
To update or view these diagrams:
- Edit the
.pumlfiles indocs/c4/. - Run
./docs/generate_diagrams.shto regenerate PNG images (requiresplantumlandgraphviz). - View the generated
.pngfiles.
Implementation Patterns
- Window construction: all UI setup is delegated to builders;
DrawingWindowshould stay thin and avoid re-accumulating menu/dock/cloud logic. - Adding a tool: (1) implement
Toolsubclass insrc/tools/creation_tools/orsrc/tools/selection_tool/, (2) add case inCanvas::setCurrentToolstring dispatch, (3) add label to tools list in the UI builder. - Tool mappings (working):
"Pen"→PenTool,"Highlighter"→HighlighterTool,"Eraser"→EraserTool,"Select"→SelectTool,"Pointer"→PointerTool,"Text"→TextTool; others are UI placeholders. - Highlighter behavior: uses current color at 30% opacity and width multiplier
x5versus the thickness slider value. - Pointer behavior: transient, collaborative "laser pointer" effect. Points have a 1-second TTL and fade out linearly. Not saved to paths or history.
- Text behavior: Markdown-capable permanent text annotations. Uses a frameless
QTextEditoverlay for input. Background set to active color at 5% opacity. Supports collaborative real-time updates and persistence. - Undoable mutations: any new canvas mutation should be wrapped in a
CanvasCommandand pushed with redo-stack clearing semantics. Use path IDs for tracking. - Ownership: Qt parent-child for widgets;
std::unique_ptr<Tool>for active tool inCanvas. - Include style: mix of relative (
../) and direct includes; match local context when editing.
Task Checklist for Agents
- Modify UI? Edit the relevant builder (
MenuBuilder,UILayoutBuilder,AIMenuBuilder,DropboxSessionBuilder,NetworkingBuilder) before touchingDrawingWindow. - Add a tool? Implement
Tool, wire inCanvas::setCurrentTool, add label in the UI builder. Assume the tool receives mapped logical coordinates, NOT native widget coordinates. For tools with complex models (like Text), also updateCollaborationProtocolandProjectLoader/Saver. - Mutate canvas trails? Always call
m_renderer.invalidateCache()andupdate()via theCanvaswhen paths are modified so the background raster updates. - Mutate canvas trails? Also register the mutation as a
CanvasCommandso it participates in Ctrl+Z/Ctrl+Shift+Z. Ensure the command lambdas emit the appropriate signals for network synchronization and use path IDs instead of indices. Deleting a selected path should use the dedicatedDeleteaction; if the selected path is the newest stroke, prefer undoing that draw command so redo preserves the stroke data. Exception:clearPathsis a hard reset and must clear history stacks instead of pushing a command. - Change persistence? Keep JSON shape intact; update both
ProjectSaverandProjectLoadersymmetrically. Now supportspathsandtextsarrays. Backward compatible with path-only files. - Build & test:
cmake --build build && ./build/SnapBoard; verify draw/select/move/undo/redo/save/load flows. - No automated tests: manual verification required for all changes.