# Widget Layouts + Usage Tracking + Config Management Research **Date:** 2026-03-01 **Sources:** - [LobsterBoard](https://github.com/Curbob/LobsterBoard) — 50+ drag-and-drop widgets, SSE, layout templates - [VidClaw](https://github.com/madrzak/vidclaw) — Soul/config editor, usage tracking, skills manager **Target:** Mosaic Stack (Next.js 15 / React 19 / NestJS / shadcn/ui / PostgreSQL) --- ## Executive Summary | Feature | LobsterBoard | VidClaw | Mosaic Stack Current | Quick Win? | |---------|--------------|---------|---------------------|------------| | Drag-and-drop widgets | ✅ Full | — | ⚠️ WidgetGrid exists, needs enabling | **Yes (30min)** | | Layout persistence | ✅ JSON to server | — | ✅ API + DB | Done | | SSE real-time | ✅ System stats | — | ✅ Already implemented | Done | | Usage widget (header) | — | ✅ Compact popover | ❌ Full page only | **Yes (30min)** | | Token parsing | — | ✅ JSONL session files | ⚠️ API-based | Low priority | | Soul/config editor | — | ✅ Multi-file + history | ❌ Not in UI | **Yes (1-2h)** | | Skills manager | — | ✅ Full CRUD + toggle | ❌ Not in UI | **Yes (1-2h)** | | Templates | ✅ Layout presets | ✅ Soul templates | ❌ None | Medium | --- ## 1. Widget System (LobsterBoard) ### Widget Registry Pattern LobsterBoard uses a global `WIDGETS` object where each widget is self-contained: ```javascript const WIDGETS = { 'weather': { name: 'Local Weather', icon: '🌡️', category: 'small', // 'small' | 'large' | 'layout' description: 'Shows current weather...', defaultWidth: 200, defaultHeight: 120, hasApiKey: false, properties: { // User-configurable defaults title: 'Local Weather', location: 'Atlanta', units: 'F', refreshInterval: 600 }, preview: `
...
`, generateHtml: (props) => `...`, generateJs: (props) => `...` }, // 50+ more widgets }; ``` **Key patterns:** 1. **Widget as code generator** — Each widget produces its own HTML + JS at render time 2. **Shared SSE** — System stats widgets share one `EventSource('/api/stats/stream')` with a callback registry 3. **Edit/View mode toggle** — Widget JS stops in edit mode, resumes in view mode 4. **20px grid snapping** — All positions snap to grid during drag 5. **Icon theming** — Dual emoji + Phosphor icon map per widget type ### Layout Persistence Schema ```json { "canvas": { "width": 1920, "height": 1080 }, "fontScale": 1.0, "widgets": [ { "id": "widget-1", "type": "weather", "x": 20, "y": 40, "width": 200, "height": 120, "properties": { "title": "Weather", "location": "Kansas City", "units": "F" } } ] } ``` Saved via `POST /config` with `Content-Type: application/json`. Loaded on startup, starts in view mode. ### What Mosaic Stack Already Has Mosaic's dashboard (`page.tsx`) already has: - ✅ `WidgetGrid` with `react-grid-layout` - ✅ `WidgetPlacement` type in `@mosaic/shared` - ✅ Layout CRUD API (`fetchDefaultLayout`, `createLayout`, `updateLayout`) - ✅ `DEFAULT_LAYOUT` for new users - ✅ Debounced auto-save on layout change (800ms) **Gap:** Widget drag-and-drop may need enabling. No dynamic widget registration or per-widget config panel yet. ### Recommendations | Priority | Feature | Effort | Impact | |----------|---------|--------|--------| | 🔴 High | Verify/enable drag-and-drop in WidgetGrid | 30min | Core UX | | 🔴 High | Widget picker modal (add/remove) | 1h | Customization | | 🟡 Med | Per-widget config dialog | 2h | Deeper customization | | 🟢 Low | Layout template presets | 2h | Onboarding | --- ## 2. Usage Tracking (VidClaw) ### Backend: JSONL Session Parsing VidClaw's `server/controllers/usage.js` reads OpenClaw session transcript files directly: ```javascript export function getUsage(req, res) { const sessionsDir = path.join(OPENCLAW_DIR, 'agents', 'main', 'sessions'); const tz = getTimezone(); const todayStart = startOfDayInTz(now, tz); const weekStart = startOfWeekInTz(now, tz); const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl')); for (const file of files) { for (const line of content.split('\n').filter(Boolean)) { const entry = JSON.parse(line); const usage = entry.message?.usage || entry.usage; if (usage?.cost?.total) { const tokens = (usage.input || 0) + (usage.output || 0) + (usage.cacheRead || 0); const cost = usage.cost.total; // Aggregate by day/week/month... } } } // Also: 5-hour rolling "session" window const SESSION_LIMIT = 45_000_000; const WEEKLY_LIMIT = 180_000_000; res.json({ model: 'claude-sonnet-4-20250514', tiers: [ { label: 'Current session', percent: 45, resetsIn: '2h 15m', tokens: 20000000, cost: 12.50 }, { label: 'Current week', percent: 32, resetsIn: '4d 8h', tokens: 58000000, cost: 38.20 } ], details: { today: { tokens, cost, sessions }, week: { tokens, cost, sessions }, month: { tokens, cost, sessions } } }); } ``` **Key design choices:** - Multi-tier limits (session 45M + weekly 180M tokens) - Timezone-aware day/week boundaries - Rolling 5-hour session window - Includes cost tracking from `usage.cost.total` ### Frontend: Compact Header Widget VidClaw's `UsageWidget.tsx` is a **popover in the header bar** — not a full page: ```tsx export default function UsageWidget() { const [expanded, setExpanded] = useState(false); const { data: usage } = useUsage(); const sessionPct = usage?.tiers?.[0]?.percent ?? 0; const pillColor = sessionPct > 80 ? 'text-red-400' : sessionPct > 60 ? 'text-amber-400' : 'text-emerald-400'; return (
{expanded && (
{/* Model selector */} {/* Progress bars per tier */} {tiers.map(tier => )}
)}
); } ``` Color coding: green (<60%), amber (60-80%), red (>80%). Includes model switcher. ### What Mosaic Stack Has Full usage page (430+ lines) with Recharts: line charts, bar charts, pie charts, time range selector. **But no compact header widget.** ### Recommendations | Priority | Feature | Effort | Impact | |----------|---------|--------|--------| | 🔴 High | Compact UsageWidget in header | 30min | Always-visible usage | | 🔴 High | Session + weekly limit % | 1h | Know quota status | | 🟡 Med | Model switcher in popover | 30min | Quick model changes | | 🟢 Low | JSONL parsing backend | 3h | Real-time session tracking | --- ## 3. Soul/Config Editor (VidClaw) ### Backend ```javascript // server/controllers/soul.js const FILE_TABS = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'AGENTS.md']; export function getSoul(req, res) { const content = fs.readFileSync(path.join(WORKSPACE, 'SOUL.md'), 'utf-8'); res.json({ content, lastModified: stat.mtime.toISOString() }); } export function putSoul(req, res) { const old = fs.readFileSync(fp, 'utf-8'); if (old) appendHistory(histPath, old); // Auto-version on every save fs.writeFileSync(fp, req.body.content); res.json({ success: true }); } export function getSoulHistory(req, res) { res.json(readHistoryFile('soul-history.json')); // Returns: [{ content, timestamp }] } export function revertSoul(req, res) { appendHistory(histPath, currentContent); // Backup before revert fs.writeFileSync(fp, history[req.body.index].content); res.json({ success: true, content }); } ``` ### Frontend `SoulEditor.tsx` (10KB) — full-featured editor: 1. **File tabs** — SOUL.md, IDENTITY.md, USER.md, AGENTS.md 2. **Code editor** — Textarea with Tab support, Ctrl+S save 3. **Right sidebar** with two tabs: - **Templates** — Pre-built soul templates, click to preview, "Use Template" to apply - **History** — Reverse-chronological versions, click to preview, hover to show "Revert" 4. **Footer** — Char count, last modified timestamp, dirty indicator, Reset/Save buttons 5. **Dirty state** — Yellow dot on tab, "Unsaved changes" warning, confirm before switching tabs ### Recommendations for Mosaic Stack | Priority | Feature | Effort | Impact | |----------|---------|--------|--------| | 🔴 High | Basic editor page with file tabs | 1h | Removes CLI dependency | | 🔴 High | Save + auto-version history | 30min | Safety net for edits | | 🟡 Med | Template sidebar | 1h | Onboarding for new users | | 🟡 Med | Preview before apply/revert | 30min | Prevent mistakes | | 🟢 Low | Syntax highlighting (Monaco) | 1h | Polish | **NestJS endpoint sketch:** ```typescript @Controller('workspace') export class WorkspaceController { @Get('file') getFile(@Query('name') name: string) { // Validate name is in allowed list // Read from workspace dir, return { content, lastModified } } @Put('file') putFile(@Query('name') name: string, @Body() body: { content: string }) { // Append old content to history JSON // Write new content } @Get('file/history') getHistory(@Query('name') name: string) { // Return history entries } } ``` --- ## 4. Skills Manager (VidClaw) ### Backend: Skill Scanning `server/lib/skills.js` scans multiple directories for skills: ```javascript const SKILL_SCAN_DIRS = { bundled: ['/opt/openclaw/skills'], managed: ['~/.config/mosaic/skills'], workspace: ['~/.openclaw/workspace/skills'] }; export function scanSkills() { const config = readOpenclawJson(); const entries = config.skills?.entries || {}; // Enabled/disabled state for (const [source, roots] of Object.entries(SKILL_SCAN_DIRS)) { for (const d of fs.readdirSync(rootDir, { withFileTypes: true })) { const content = fs.readFileSync(path.join(d.name, 'SKILL.md'), 'utf-8'); const fm = parseFrontmatter(content); // Parse YAML frontmatter skills.push({ id: d.name, name: fm.name || d.name, description: fm.description || '', source, // 'bundled' | 'managed' | 'workspace' enabled: entries[id]?.enabled ?? true, path: skillPath, }); } } return skills; } ``` ### Backend: CRUD ```javascript // Toggle: writes to openclaw.json config export function toggleSkill(req, res) { config.skills.entries[id] = { enabled: !current }; writeOpenclawJson(config); } // Create: writes SKILL.md with frontmatter export function createSkill(req, res) { const dir = path.join(SKILLS_DIRS.workspace, name); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${desc}\n---\n\n${instructions}`); } // Delete: workspace skills only export function deleteSkill(req, res) { if (skill.source !== 'workspace') return res.status(403); fs.rmSync(skill.path, { recursive: true }); } ``` ### Frontend `SkillsManager.tsx` (12KB): 1. **Stats cards** — Total, Enabled, Bundled, Workspace counts 2. **Filters** — Search, source filter dropdown, status filter dropdown 3. **Skill cards** — Name + source badge + toggle switch + expand/collapse 4. **Expanded view** — Shows full SKILL.md content (lazy-loaded) 5. **Create modal** — Name (slug), description, instructions (markdown textarea) 6. **Source badges** — Color-coded: blue=bundled, orange=managed, green=workspace 7. **Delete** — Only workspace skills, with confirmation ### Recommendations for Mosaic Stack | Priority | Feature | Effort | Impact | |----------|---------|--------|--------| | 🔴 High | Skills list with toggle | 1h | Visibility + control | | 🟡 Med | Create skill modal | 1h | No CLI needed | | 🟡 Med | Skill content viewer | 30min | See what skills do | | 🟢 Low | Search + filters | 30min | Polish for 100+ skills | --- ## 5. Quick Wins — Prioritized Implementation Plan ### 🚀 #1: Compact Usage Widget in Header (30 min) - Create `components/UsageWidget.tsx` using shadcn `Popover` + `Progress` - Reuse existing `useUsageSummary` hook - Add to authenticated layout header - Color-code: green/amber/red based on percentage ### 🚀 #2: Enable Widget Drag-and-Drop (30 min) - Check `WidgetGrid` for `isDraggable`/`static` props - Enable drag + resize in react-grid-layout - Verify auto-save still works after moves ### 🚀 #3: Soul Editor Page (1-2h) - New page: `settings/soul/page.tsx` - File tabs: SOUL.md, IDENTITY.md, USER.md, AGENTS.md - Backend: `GET/PUT /api/workspace/file?name=SOUL.md` - Auto-version history on save - Simple Textarea with Save button ### 🚀 #4: Skills List + Toggle (1-2h) - New page: `settings/skills/page.tsx` - Backend: `GET /api/skills`, `POST /api/skills/:id/toggle` - Scan skill directories, parse frontmatter - Toggle switch per skill using shadcn `Switch` ### 🚀 #5: Dashboard Empty State (30 min) - Show "Add your first widget" card when layout is empty - Link to widget picker **Total estimated effort for all 5: ~4-5 hours for a dramatically more complete UI.** --- ## 6. Schemas Worth Borrowing ### Skill Type (for Mosaic Stack shared package) ```typescript interface Skill { id: string; name: string; description: string; source: 'bundled' | 'managed' | 'workspace'; enabled: boolean; path: string; } ``` ### Usage Tier Type ```typescript interface UsageTier { label: string; percent: number; resetsIn: string; tokens: number; cost: number; } ``` ### Widget Definition Type (if building registry) ```typescript interface WidgetDefinition { id: string; name: string; icon: string; category: 'kpi' | 'chart' | 'list' | 'system'; description: string; defaultSize: { w: number; h: number }; configSchema?: Record; component: React.ComponentType; } ``` --- ## Key File References ### LobsterBoard - `js/widgets.js` — 50+ widget definitions with HTML/JS generators - `js/builder.js` — Canvas, drag-drop, resize, edit/view mode, config save/load ### VidClaw - `server/controllers/usage.js` — JSONL token parsing, multi-tier limits - `server/controllers/soul.js` — SOUL.md CRUD + version history - `server/controllers/skills.js` — Skills CRUD (toggle, create, delete) - `server/lib/skills.js` — Directory scanning + frontmatter parsing - `src/components/Usage/UsageWidget.tsx` — Compact header usage popover - `src/components/Soul/SoulEditor.tsx` — Multi-file editor with history + templates - `src/components/Skills/SkillsManager.tsx` — Skills list, filter, toggle, create --- *Research completed 2026-03-01 by subagent for Mosaic Stack development.*