466 lines
15 KiB
Markdown
466 lines
15 KiB
Markdown
# 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: `<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
|
|
|
|
```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 (
|
|
<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
|
|
|
|
```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<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.*
|