Files
stack/docs/research/02-widgets-usage-config-research.md
2026-03-01 16:08:40 -06:00

15 KiB

Widget Layouts + Usage Tracking + Config Management Research

Date: 2026-03-01 Sources:

  • LobsterBoard — 50+ drag-and-drop widgets, SSE, layout templates
  • 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:

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: `<div>...</div>`,
    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

{
  "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:

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:

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 (
    <div className="relative">
      <button onClick={() => setExpanded(!expanded)}
        className="flex items-center gap-2 bg-secondary/50 rounded-full px-4 py-1.5 text-xs">
        <Zap size={12} className="text-orange-400" />
        <span className="text-muted-foreground">{model}</span>
        <div className="w-16 h-1.5 bg-secondary rounded-full overflow-hidden">
          <div className={barColor} style={{ width: `${sessionPct}%` }} />
        </div>
        <span className={pillColor}>{sessionPct}%</span>
      </button>
      
      {expanded && (
        <div className="absolute right-0 top-full mt-2 w-80 bg-card border rounded-lg shadow-xl p-4">
          {/* Model selector */}
          <select value={model} onChange={switchModel}>...</select>
          {/* Progress bars per tier */}
          {tiers.map(tier => <ProgressBar key={tier.label} {...tier} />)}
        </div>
      )}
    </div>
  );
}

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

// 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:

@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:

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

// 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)

interface Skill {
  id: string;
  name: string;
  description: string;
  source: 'bundled' | 'managed' | 'workspace';
  enabled: boolean;
  path: string;
}

Usage Tier Type

interface UsageTier {
  label: string;
  percent: number;
  resetsIn: string;
  tokens: number;
  cost: number;
}

Widget Definition Type (if building registry)

interface WidgetDefinition {
  id: string;
  name: string;
  icon: string;
  category: 'kpi' | 'chart' | 'list' | 'system';
  description: string;
  defaultSize: { w: number; h: number };
  configSchema?: Record<string, { type: string; label: string; default: unknown }>;
  component: React.ComponentType<WidgetProps>;
}

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.