unDrifter — Agent Context Guide
Version: Bumbler (v0.2.0)
This document provides context for AI agents or developers continuing work on unDrifter. It captures architectural decisions, data flow, and implementation details that aren't obvious from code alone.
---
Agent Workflow: Small Achievable Bites
Work in tight, shippable increments. This reduces crash recovery cost and keeps the system in a working state.
1. Check the backlog — Before starting, read docs/backlog/SPRINT_BACKLOG.md for the current task list.
2. One task at a time — Pick a single task (one row in the sprint backlog). Do not batch multiple tasks unless they are trivial and inseparable.
3. Slice small — If a task takes more than ~15–20 minutes or spans many files, split it. Add the new sub-tasks to the sprint backlog.
4. Verify before moving on — Each task should leave the system runnable. Run the server, test the flow, fix broken paths.
5. Update the backlog — Mark tasks done in SPRINT_BACKLOG.md. Add discovered tasks to PRODUCT_BACKLOG.md or SPRINT_BACKLOG.md as needed.
6. Commit checkpoints — Commit after each completed task so you can revert if needed.
If Cursor crashes: SPRINT_BACKLOG.md and PRODUCT_BACKLOG.md are your anchors. Resume from the next unchecked task.
---
What Is unDrifter?
unDrifter extracts structured content from webpages and injects it into FigJam/Figma as editable text nodes. It's a "Living Copy Doc" tool for designers and content strategists.
The core insight: Don't try to recreate layouts. Extract the content (headings, paragraphs, lists, quotes, images, links) and let designers work with it in their native environment.
---
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ USER'S MACHINE │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Chrome Extension │────▶│ Local Server │◀────│ Figma Plugin │ │
│ │ (content capture)│POST │ (data bridge) │ GET │ (node create)│ │
│ └──────────────────┘ │ Variables API │ │ Sync vars │ │
│ └────────▲────────┘ └──────────────┘ │
│ ┌──────────────────┐ │ │
│ │ FigJam Widget │──GET/PATCH────┘ │
│ │ Create CopyDoc │ POST /figjam/items │
│ │ Edit variables │ GET/PATCH /variables │
│ │ Link Figma file │ │
│ └──────────────────┘ data/copydoc.json │
└─────────────────────────────────────────────────────────────────────┘
Why This Architecture?
1. Figma REST API can't create nodes — It's read-only for file content. You must use the Plugin API to create frames, text, etc.
2. Chrome extension can't talk directly to plugin — They're in different contexts. The local server acts as a bridge.
3. Local-first for privacy — No cloud dependency. User's content never leaves their machine.
---
Data Flow
Capture Flow (Extension → Server)
1. User clicks extension icon → overlay activates 2. User chooses mode and defines bounds:
- Select: Click on page → bounds from topmost element at point (with parent/child arrows)
- Draw: Drag full-width band → bounds = viewport width × band height
- Full Page: Click Full Page button → scrolls to bottom (loads lazy content), scrolls back to top, bounds = full document size
4. Expand Show More (Select/Draw only; Full Page skips entirely) → opens <details>, clicks "Show More" buttons, accordions; skips gallery/carousel/tab; skips links that navigate away. Full-page: scroll only, no interaction.
5. DOM nodes within bounds are extracted → semantic nodes only (including collapsed body copy when hidden in DOM)
6. Semantic inference runs → detects roles, components, classes
7. Extraction converts to ChildNode[] with semantics metadata
8. Screenshot captured via chrome.tabs.captureVisibleTab() → cropped to bounds
9. Zone added to local queue (Zustand store)
10. User clicks "Push to FigJam" → POST /zones to server
11. Server stores in data/copydoc.json
Injection Flow (Server → Plugin)
1. User opens FigJam/Figma plugin 2. Plugin fetches GET /zones 3. Plugin creates nodes for each zone:
- Section/Frame (container)
- Title text
- Metadata text
- Screenshot image (via proxy if needed)
- Child nodes (heading/paragraph/list/etc.)
Paste & Link Flow (Plugin)
1. User selects a text layer in Figma 2. Plugin shows selection info (text layer or component instance) 3. User clicks "Paste & Link" on content item 4. Plugin pastes text and stores link metadata:
setSharedPluginData()with namespace "undrifter"setRelaunchData()for inspector buttons
Sync Flow (Plugin)
1. User clicks "Sync Links" or uses inspector button 2. Plugin finds all nodes with undrifter shared data 3. Fetches current zones from server 4. Updates text content from matching childId 5. Preserves font styling
URL-Based Zone Management Flow
1. Zones are grouped by source.url in plugin UI
2. User clicks URL link → opens with ?undrifter=load query param
3. Extension detects query param on page load
4. Extension fetches zones for current URL (normalized, no query/hash)
5. Zones loaded into extension state automatically
6. User can edit zone names or delete zones
7. Changes sync to server via PUT/DELETE endpoints
8. Plugin can delete all zones for a URL via DELETE /zones/by-url
Variables Flow (Plugin)
1. User clicks "📊 Vars" button
2. Plugin creates/updates VariableCollection named "CopyDoc"
3. Each content item becomes a STRING variable
4. Named: {zoneName}/{type}-{id}
5. Users can bind variables to text layers natively
6. Sync vars — Plugin fetches GET /variables, updates existing variables or creates new ones
FigJam Widget Flow
1. Widget runs in FigJam board; fetches GET /variables from server (scoped by figjamFileKey) 2. Create from FigJam — User adds variables (name optional, value); POST /figjam/items creates "FigJam Copy" zone; Add spawns variable stickies via cloneWidget (one per variable, freely on board) 3. Refresh — Loads variables; creates stickies for variables that don't have widgets yet (findWidgetNodesByWidgetId) 4. Control vs variable mode — Branch on childId in synced state; variable stickies show single variable with status cycle 5. Edit — User edits sticky notes; on blur/Enter, PATCH /variables updates server 6. Link Figma — User pastes Figma design file URL/key; POST /file-bindings links FigJam board to Figma file 7. Sync in Figma — Plugin "Sync vars" pulls latest values; variables update; bound text layers refresh
---
Key Data Types
Defined in shared/types.ts:
typescript
interface Zone {
id: string; // UUID
name: string; // User-provided label
order: number; // Reading order
bounds: ZoneBounds; // { x, y, width, height } in document coords
source: ZoneSource; // { url, title, capturedAt }
children: ChildNode[]; // Extracted/manual content
screenshot?: string; // Base64 PNG
}
interface ChildNode {
id: string;
type: 'heading' | 'paragraph' | 'list' | 'quote' | 'image' | 'link';
source: 'extracted' | 'manual';
createdAt: string;
semantics?: Semantics; // Inferred metadata
// + type-specific fields (text, level, items, src, href, etc.)
}
interface Semantics {
role?: string; // ARIA role or inferred role
component?: string; // Detected component type (hero, nav, card, etc.)
classes?: string[]; // Notable CSS classes
path?: string; // DOM path to element
container?: string; // Parent container type
layout?: 'sticky' | 'fixed'; // position: sticky/fixed from computed style (shown in outline)
}
interface CopyDoc {
docId: string;
schemaVersion: 1;
zones: Zone[];
}
---
Component Details
Chrome Extension (/extension)
Stack: MV3 + React + Vite + CRXJS + Zustand
| File | Purpose |
| ------ | --------- |
src/background/index.ts | Service worker: icon click, screenshot capture |
src/content/index.tsx | Overlay UI: selection, crop handles, preview |
src/content/extract.ts | DOM extraction + semantic inference |
src/store/zoneStore.ts | Zustand store: zone queue with persistence |
Key Features:
- Three capture modes — Select (click to detect container), Draw (full-width band), Full Page (scrolls to load lazy content, then captures entire page; no expand/interaction — avoids modals, gallery)
- Crop-style selection — vertical band selection with top/bottom handles (Select/Draw)
- Expand Show More (Select/Draw only; full-page skips) — clicks accordions and "Show More"; skips gallery/carousel/tab; skips links that navigate away
- Visual-only by default — extraction skips
display:none,visibility:hidden, andhidden; no collapsed "Show more" content is added, so full-page and all captures match what the user sees (shorter zones, no 66k px columns). SetvisualOnly: falsein extract options to include collapsed body copy inside containers. - Semantic extraction — h1-h6, p, ul/ol, blockquote, img, a, button, figcaption, span (standalone), div (leaf text only, e.g. eyebrow/body)
- Semantic inference — ARIA roles, common class patterns, data attributes; layout (position: sticky/fixed) captured and shown in outline hierarchy
- Full-page structure — When capturing full page, top-level wrapped in page template; content grouped into semantic chunks (hero, nav, section, footer)
- Repeated structures — Runs of 2+ groups with same component (e.g. cards) wrapped in template group (e.g. card-list)
- Smart fallback — expands bounds or drills into containers if zone appears empty
- Whitespace normalization — clean text extraction at source
- Accessibility filtering — automatically skips visually hidden a11y content
- Shadow DOM — style isolation from page
- Document coordinates — viewport + scroll offset
- Zone loading from URLs — detects
?undrifter=loadand auto-loads zones - Edit zones — rename zones, syncs to server via PUT
- Delete zones — remove zones, syncs to server via DELETE
- Oy branding — mascot image in panel header
Semantic Inference Patterns:
typescript
COMPONENT_PATTERNS = {
hero: /hero|banner|jumbotron|splash/i,
nav: /nav|menu|navigation|topbar|sidebar/i,
card: /card|tile|item|product/i,
footer: /footer|bottom/i,
header: /header|masthead/i,
cta: /cta|call-to-action|action|button/i,
form: /form|input|field/i,
modal: /modal|dialog|popup|overlay/i,
list: /list|grid|gallery/i,
article: /article|post|blog|content/i,
}
Local Server (/server)
Stack: Node.js + Hono + tsx (or Bun)
| Endpoint | Method | Purpose |
| ---------- | -------- | --------- |
/ | GET | Health check |
/zones | GET | Fetch all zones |
/zones | POST | Create zones (from extension) |
/zones | DELETE | Delete all zones |
/zones/:id | GET | Get single zone |
/zones/:id | PUT | Update zone (e.g., rename) |
/zones/:id | DELETE | Delete zone |
/zones/by-url | GET | Get zones for specific URL |
/zones/by-url | DELETE | Delete all zones for URL |
/zones/:id/children | POST | Add manual content |
/variables | GET | Variable items (widget, plugin sync) |
/variables | PATCH | Update variable values (FigJam edits) |
/figjam/items | POST | Create CopyDoc items from FigJam |
/file-bindings | GET/POST | FigJam ↔ Figma file bindings |
/copydoc | GET | Get full CopyDoc |
/screenshots/:file | GET | Serve saved zone screenshot (file on disk) |
/proxy/image | GET | Proxy external images (CORS bypass) |
/enrich/* | GET/POST | Ollama or OpenClaw: status, test, zone-name, variable-names, block-types, semantics (AI-inferred naming; forceReEnrich to override script labels), clean-structure, structure-examples, chat (text + optional image), create-zone, create-zone-from-reply (see docs/AI_FEATURES.md) |
/logs | GET | Server log buffer (last 500 lines; landing Logs tab) |
/auth/login | GET | Start Figma OAuth |
/auth/callback | GET | OAuth callback |
/auth/status | GET | Check auth status |
/auth/logout | POST | Clear auth |
/auth/token | GET | Get current token |
Key Features:
- CORS allows
https://www.figma.com, Chrome extensions, localhost - Data persists to
data/copydoc.json - Image proxy fetches external images for Figma
- OAuth with CSRF protection
FigJam/Figma Plugin (/plugin)
Stack: Figma Plugin API + Vite
| File | Purpose |
| ------ | --------- |
src/code.ts | Plugin API code: node creation, linking, variables |
src/ui.html | Plugin UI: docs, links, search views |
manifest.json | Plugin config + relaunch buttons |
Key Features:
#### Shared Plugin Data
typescript
PLUGIN_NAMESPACE = 'undrifter';
DATA_KEYS = { childId, zoneId, zoneName, linkedAt, contentType };
// Uses setSharedPluginData() — readable by other plugins
#### RelaunchData (Inspector Buttons)
typescript
node.setRelaunchData({
'open': 'View in CopyDoc',
'sync': 'Sync from source',
});
// manifest.json has relaunchButtons config
#### Component Property Support
typescript
if (node.type === 'INSTANCE') {
const props = node.componentProperties;
// Find TEXT properties, use setProperties() to update
}
#### Figma Variables
typescript
const collection = figma.variables.createVariableCollection('CopyDoc');
const variable = figma.variables.createVariable(name, collection, 'STRING');
variable.setValueForMode(modeId, text);
Important Figma JS Limitations:
- No spread syntax in object literals:
{...obj}fails - No
for...ofon Sets - No async generators
- Use explicit property assignment instead
FigJam Widget (/widget)
Stack: Figma Widget API + esbuild (no React)
| File | Purpose |
| ------ | --------- |
src/code.tsx | Widget UI: control mode (Add, Refresh, Link) + variable mode (single sticky) |
manifest.json | editorType: figjam, networkAccess: localhost:3001 |
Key Features:
- Add variable — POST /figjam/items creates zone; cloneWidget spawns one sticky per variable on board
- Refresh — GET /variables; findWidgetNodesByWidgetId; creates stickies for variables without widgets
- Control vs variable mode — Branch on childId; variable stickies show single variable with status cycle
- Status — Draft/Review/Approved with colors; click to cycle; PATCH with status
- Edit — PATCH /variables on blur/Enter
- Link Figma — POST /file-bindings (figjamFileKey, figmaFileKey)
- figma.fileKey — Often null in dev; widget shows manual paste for FigJam board key
---
Plugin UI Structure
Main nav tabs (icon stacked above label for all): Docs | Blank | Links | Structure | Settings
- Docs — Tree of zones and children; global filter bar; "Search by path" (semantic selector) in CopyDoc panel; paste/copy, zone actions (Enrich, Inject, Delete as icon buttons), screenshot preview, CSS tokens when enabled. Default tab.
- Blank — Start from blank: create zones from a content model (name/type, description, optional annotation); Suggest name and Suggest types (AI) when enabled in Settings.
- Links — All linked layers with sync/unlink.
- Settings — Feature flags (progressive enhancements): AI enrichment on capture (clean-structure + semantics on capture when screenshot present), screenshot on capture, show CSS tokens in docs, Suggest name/types in Blank, Debug log. Stored in localStorage.
Actions (footer): Refresh | Sync | Vars | Sync vars | Inject All to Canvas
UI Details:
- URL Grouping: Zones automatically grouped by
source.url - URL Links: Full URL displayed, clickable to open in browser
- Copy: Click any content row to copy plain text (normalized whitespace)
- Image Handling: Thumbnails via proxy or server screenshot URL; "Place on Canvas" creates Figma image node
- Injection width: If a single node (e.g. frame) is selected when injecting, zone section width uses that node's width (200–800px) so content wraps to match layout
- Whitespace Normalization: Applied in extraction and UI display
- Oy Branding: Mascot image in header (no text), also in empty state
---
Gotchas & Edge Cases
Coordinate Spaces
getBoundingClientRect()returns viewport-relative- Add
window.scrollX/Yfor document coordinates - Convert back to viewport for screenshot cropping
FigJam vs Figma
- Both use same API, some node types differ
- Sections only in FigJam
- Check
figma.editorType
Empty Zones
- Confirmation dialog before capturing empty regions
- Proceeds if user confirms
- Smart fallback: expands bounds by 10%/25%, drills into containers, walks up DOM tree
- Filters visually hidden accessibility content automatically
Screenshots
- Server can save zone screenshots to disk (
data/screenshots/), setzone.screenshotUrl, and serve via GET/screenshots/:file - Plugin fetches from screenshotUrl when injecting; extension can show same URL
- Inline base64 in JSON still supported for backward compatibility
Plugin Network Access
- Must declare
networkAccess.allowedDomainsin manifest - Full URL with scheme:
https://localhost:3001
URL Normalization
- Zones store normalized URLs (no query params or hash)
- Extension normalizes URLs when creating zones
- Extension normalizes when loading zones for matching
- Ensures consistent matching across page loads
Image Loading
- External images need CORS proxy
- Server's
/proxy/imageendpoint fetches and returns bytes - Plugin uses
figma.createImage()with Uint8Array
---
Testing Checklist
- [ ] Extension loads without errors
- [ ] Selection overlay appears on click
- [ ] Crop handles resize correctly
- [ ] DOM extraction captures semantic nodes
- [ ] Screenshot captures selected region
- [ ] Push to FigJam succeeds
- [ ] Plugin loads zones
- [ ] Inject creates nodes on canvas
- [ ] Paste & Link stores metadata
- [ ] Inspector buttons appear
- [ ] Sync updates linked content
- [ ] Variables created in collection
- [ ] Image proxy works for external URLs
- [ ] Zones grouped by URL in plugin
- [ ] URL links open browser with ?undrifter=load
- [ ] Extension loads zones automatically
- [ ] Edit/delete zones in extension syncs to server
- [ ] Delete by URL works in plugin
- [ ] Whitespace normalized in text
- [ ] Accessibility text filtered out
- [ ] Single zone injection works
- [ ] Copy to clipboard works
- [ ] Image placement works
---
Future Enhancements
Tracked in docs/backlog/PRODUCT_BACKLOG.md. Summary:
1. Multiple CopyDocs / Instances — Done; per-instance storage, widget/plugin scoped by figjamFileKey/figmaFileKey 2. Cloud sync — Optional backend for cross-device access 3. Dev Mode codegen — Show CopyDoc refs in Dev Mode 4. Variable binding UI — Bind layers to variables in plugin 5. Readability.js — Better article extraction
---
Backlog
- Product backlog —
docs/backlog/PRODUCT_BACKLOG.md(themes, epics, features) - Sprint backlog —
docs/backlog/SPRINT_BACKLOG.md(current small tasks)
---
Development Commands
bash
Server (run first)
cd server && npm run dev
Extension
cd extension && npm run dev # Watch mode
cd extension && npm run build # Production
Plugin
cd plugin && npm run dev # Watch mode
cd plugin && npm run build # Production
FigJam Widget
cd widget && npm run build # Or: npm run build:widget from repo root
Load extension from extension/dist in chrome://extensions.
Internal layout demos are not linked from the product hub; direct URLs on a running server: /token-demo/token-demo.html, /token-demo/brand-typography-explorer.html.
Link plugin via Figma → Resources → Plugins → Development → Link existing.
Import widget via FigJam → Resources → Widgets → Development → Import widget from manifest → select widget/manifest.json.
---
Version History
| Version | Name | Notes |
| --------- | ------ | ------- |
| 0.1.0 | Throcken | Initial implementation |
| 0.1.1 | Throcken | Native Figma integration: Variables, RelaunchData, Component Properties, Shared Plugin Data, crop handles, semantic inference, zones preserved after injection |
| 0.1.2 | Throcken | URL-based management, zone loading from links, edit/delete in extension, smart fallback, whitespace normalization, a11y filtering, Oy branding, single zone injection, copy to clipboard, image placement |
| 0.2.0 | Bumbler | Ollama integration: zone-name enrichment (local LLM); Bumbler version naming |
| 0.2.x | Bumbler | Settings view (feature flags); injection layout width from selection; span/div extraction (eyebrow, body); screenshot save to server; zone row icon-only actions; nav tabs icon-above-label; Full Page capture (scroll-to-load); expand Show More (skips external links); visual-only extraction (no display:none/hidden; no collapsed body copy by default) so zones match visible content |
| 0.2.x | Bumbler | FigJam widget — create CopyDoc in FigJam (POST /figjam/items), sticky-note UI, edit variables (PATCH /variables), link to Figma (file bindings), Sync vars in plugin |
| 0.2.x | Bumbler | Variable stickies — cloneWidget on Add; Refresh creates stickies; control vs variable mode; status workflow. Full-page no-interaction; expand skips gallery/carousel. |
| 0.2.x | Bumbler | Landing page — Collapse all in zone structure view; dual-port HTTP/HTTPS (FigJam widget on 3002); fix fetch/onclick syntax errors. |
---
"Go then, there are other worlds than these."