← unDrifter

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:

3. Crop handles appear for adjustment → confirm/cancel buttons (Select/Draw)

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:

4. Zones are preserved on server (not deleted) for re-sync

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:

5. Content is now linked to CopyDoc source

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

FilePurpose
---------------
src/background/index.tsService worker: icon click, screenshot capture
src/content/index.tsxOverlay UI: selection, crop handles, preview
src/content/extract.tsDOM extraction + semantic inference
src/store/zoneStore.tsZustand store: zone queue with persistence

Key Features:

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)

EndpointMethodPurpose
---------------------------
/GETHealth check
/zonesGETFetch all zones
/zonesPOSTCreate zones (from extension)
/zonesDELETEDelete all zones
/zones/:idGETGet single zone
/zones/:idPUTUpdate zone (e.g., rename)
/zones/:idDELETEDelete zone
/zones/by-urlGETGet zones for specific URL
/zones/by-urlDELETEDelete all zones for URL
/zones/:id/childrenPOSTAdd manual content
/variablesGETVariable items (widget, plugin sync)
/variablesPATCHUpdate variable values (FigJam edits)
/figjam/itemsPOSTCreate CopyDoc items from FigJam
/file-bindingsGET/POSTFigJam ↔ Figma file bindings
/copydocGETGet full CopyDoc
/screenshots/:fileGETServe saved zone screenshot (file on disk)
/proxy/imageGETProxy external images (CORS bypass)
/enrich/*GET/POSTOllama 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)
/logsGETServer log buffer (last 500 lines; landing Logs tab)
/auth/loginGETStart Figma OAuth
/auth/callbackGETOAuth callback
/auth/statusGETCheck auth status
/auth/logoutPOSTClear auth
/auth/tokenGETGet current token

Key Features:

FigJam/Figma Plugin (/plugin)

Stack: Figma Plugin API + Vite

FilePurpose
---------------
src/code.tsPlugin API code: node creation, linking, variables
src/ui.htmlPlugin UI: docs, links, search views
manifest.jsonPlugin 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:

FigJam Widget (/widget)

Stack: Figma Widget API + esbuild (no React)

FilePurpose
---------------
src/code.tsxWidget UI: control mode (Add, Refresh, Link) + variable mode (single sticky)
manifest.jsoneditorType: figjam, networkAccess: localhost:3001

Key Features:

---

Plugin UI Structure

Main nav tabs (icon stacked above label for all): Docs | Blank | Links | Structure | Settings

Actions (footer): Refresh | Sync | Vars | Sync vars | Inject All to Canvas

UI Details:

---

Gotchas & Edge Cases

Coordinate Spaces

FigJam vs Figma

Empty Zones

Screenshots

Plugin Network Access

URL Normalization

Image Loading

---

Testing Checklist

---

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

---

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

VersionNameNotes
----------------------
0.1.0ThrockenInitial implementation
0.1.1ThrockenNative Figma integration: Variables, RelaunchData, Component Properties, Shared Plugin Data, crop handles, semantic inference, zones preserved after injection
0.1.2ThrockenURL-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.0BumblerOllama integration: zone-name enrichment (local LLM); Bumbler version naming
0.2.xBumblerSettings 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.xBumblerFigJam 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.xBumblerVariable stickies — cloneWidget on Add; Refresh creates stickies; control vs variable mode; status workflow. Full-page no-interaction; expand skips gallery/carousel.
0.2.xBumblerLanding 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."