feat(frontend): BeOS icons as agent activity indicators, remove background scatter

This commit is contained in:
engineering Agent 2026-05-19 19:11:38 -07:00
parent 63ce6ac1fa
commit 399c7958ff

View file

@ -46,71 +46,59 @@ function spawnSparkle(x, y) {
if (sparkles.length > 60) sparkles.shift();
}
// ── BeOS tile background system ──────────────────────────────────
const beosTiles = [];
// ── BeOS activity icons ──────────────────────────────────────────
const activityIcons = {};
const ACTIVITY_ICONS = [
'dispatch', 'code', 'build', 'review', 'research', 'analyze',
'write', 'security', 'monitor', 'memory', 'coach', 'heartbeat',
'mail', 'collaborate', 'present', 'vault', 'think', 'idle',
'blocked', 'document', 'terminal', 'package', 'network',
'search', 'design', 'data',
];
function loadBeosTiles() {
const tileNames = [
'bell', 'email', 'clock', 'folder-open', 'folder-drag-accept',
'audio-volume-high', 'audio-volume-medium', 'audio-volume-muted',
'bluetooth-active', 'firefox08icon', 'firebirdicon',
'accessories-calculator', 'accessories-text-editor',
'GameBoy_1_32x32x8', 'GameBoy 2_1_32x32x8',
'alarm-clock-icon', 'config-users', 'Cinder-Block-icon',
'appointment-missed', 'appointment-soon',
];
for (const name of tileNames) {
function loadActivityIcons() {
ACTIVITY_ICONS.forEach(name => {
const img = new Image();
img.src = `/office/beos-tiles/${name}.png`;
beosTiles.push(img);
}
img.src = `/office/beos-icons/${name}.png`;
activityIcons[name] = img;
});
}
// Track placed BeOS tile positions (computed once)
let beosPlacements = null;
let beosPlacementsComputed = false;
// Map agent state → BeOS icon name
function getActivityIcon(agent) {
const activity = agent.activity || 'idle';
function computeBeosPlacements() {
if (beosPlacementsComputed) return;
beosPlacementsComputed = true;
beosPlacements = [];
// Sparse grid: every 4 tiles horizontally, every 3 vertically
const gridSpacingX = 4;
const gridSpacingY = 3;
for (let row = 0; row < ROWS; row += gridSpacingY) {
for (let col = 0; col < COLS; col += gridSpacingX) {
const px = col * TILE_SIZE;
const py = row * TILE_SIZE;
// Skip dirt path rows
const onHPath = (py >= 48 && py < 64) || (py >= 224 && py < 240) || (py >= 544 && py < 560);
const onVPath = (col >= 6 && col < 7) || (col >= 13 && col < 14) || (col >= 22 && col < 23);
if (onHPath || onVPath) continue;
// Skip building areas
let inBuilding = false;
for (const b of BUILDINGS) {
if (col >= b.col && col < b.col + b.w && row >= b.row && row < b.row + b.h) {
inBuilding = true;
break;
}
}
if (inBuilding) continue;
const tileIdx = Math.floor(Math.random() * beosTiles.length);
beosPlacements.push({ px, py, tileIdx });
}
}
}
if (activity === 'idle' || activity === 'offline') return 'idle';
if (activity === 'blocked') return 'blocked';
if (activity === 'thinking') return 'think';
function drawBeosTiles(ctx) {
if (!beosPlacementsComputed) computeBeosPlacements();
if (!beosPlacements) return;
ctx.globalAlpha = 0.3;
for (const p of beosPlacements) {
const img = beosTiles[p.tileIdx];
if (img && img.complete && img.naturalWidth > 0) {
ctx.drawImage(img, p.px, p.py, TILE_SIZE, TILE_SIZE);
}
}
ctx.globalAlpha = 1;
// activity === 'working' → check currentTask keywords
const task = (agent.currentTask || '').toLowerCase();
if (/\b(dispatch|rout)\b/.test(task)) return 'dispatch';
if (/\b(code|implement|build|pr)\b/.test(task)) return /\bbuild\b/.test(task) ? 'build' : 'code';
if (/\b(review|audit)\b/.test(task)) return 'review';
if (/\bresearch\b/.test(task)) return 'research';
if (/\bsearch\b/.test(task)) return 'search';
if (/\banalyz\b/.test(task)) return 'analyze';
if (/\b(writ|draft|content|blog)\b/.test(task)) return 'write';
if (/\bsecurit|complianc\b/.test(task)) return 'security';
if (/\bmonitor\b/.test(task)) return 'monitor';
if (/\bhealth|check\b/.test(task)) return 'heartbeat';
if (/\b(memory|index|vault|note)\b/.test(task)) return /\bvault\b/.test(task) ? 'vault' : 'memory';
if (/\b(coach|learn|improv)\b/.test(task)) return 'coach';
if (/\b(mail|message|communicat)\b/.test(task)) return 'mail';
if (/\b(collaborat|pair|meeting)\b/.test(task)) return 'collaborate';
if (/\b(present|slide|deck)\b/.test(task)) return 'present';
if (/\b(document|spec|readme)\b/.test(task)) return 'document';
if (/\b(terminal|cli|script)\b/.test(task)) return 'terminal';
if (/\b(deploy|package|release)\b/.test(task)) return 'package';
if (/\b(network|connect|api)\b/.test(task)) return 'network';
if (/\b(design|layout|ui|css)\b/.test(task)) return 'design';
if (/\b(data|stats)\b/.test(task)) return 'data';
// Default: code icon for working agents
return 'code';
}
// ── Draw helpers ──────────────────────────────────────────────────
@ -401,6 +389,16 @@ function drawAgent(ctx, agent, time) {
ctx.restore();
// ── Activity icon above agent ──────────────────────────────
const iconName = getActivityIcon(agent);
const iconImg = activityIcons[iconName];
if (iconImg && iconImg.complete && iconImg.naturalWidth > 0) {
ctx.save();
ctx.globalAlpha = 1;
ctx.drawImage(iconImg, px - 12, py + yOffset - 60, 24, 24);
ctx.restore();
}
// Blocked: red tint overlay
if (activity === 'blocked') {
ctx.fillStyle = 'rgba(255,50,50,0.3)';
@ -519,9 +517,9 @@ export default function FarmCanvas({ onAgentClick, onAgentHover }) {
}
}, [onAgentClick]);
// Load BeOS tiles on mount
// Load activity icons on mount
useEffect(() => {
loadBeosTiles();
loadActivityIcons();
}, []);
// Animation loop
@ -538,7 +536,6 @@ export default function FarmCanvas({ onAgentClick, onAgentHover }) {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
drawGrass(ctx);
drawBeosTiles(ctx);
drawDirtPaths(ctx);
for (const b of BUILDINGS) drawBuilding(ctx, b);
drawCampfire(ctx, time);
@ -643,8 +640,8 @@ export default function FarmCanvas({ onAgentClick, onAgentHover }) {
display: 'block',
imageRendering: 'pixelated',
cursor: hoveredAgent ? 'pointer' : 'default',
maxWidth: '100%',
maxHeight: '100%',
width: '100%',
height: '100%',
objectFit: 'contain',
}}
/>