feat: show screen thumbnails in pod streams

This commit is contained in:
Yahya Alhinai
2026-06-28 05:33:00 +00:00
parent 1080bfd85f
commit c771e1594c
6 changed files with 39 additions and 8 deletions
+1
View File
@@ -47,6 +47,7 @@ function observationEvent(doc: EngineerContext): PodActivityEvent {
actor: doc.engineerId, actor: doc.engineerId,
actors: [doc.engineerId], actors: [doc.engineerId],
file, file,
imageUrl: doc.screenshotDataUrl,
title: file ? `Working in ${file}` : 'Screen context updated', title: file ? `Working in ${file}` : 'Screen context updated',
detail: [ detail: [
symbol ? `symbol ${symbol}` : undefined, symbol ? `symbol ${symbol}` : undefined,
+16 -5
View File
@@ -20,6 +20,7 @@ import { initMemory } from './memory/db.js';
const POD_ROOM = process.env.POD_ROOM ?? 'demo-pod'; const POD_ROOM = process.env.POD_ROOM ?? 'demo-pod';
const HERMES_IDENTITY = 'podman-hermes'; const HERMES_IDENTITY = 'podman-hermes';
const SAMPLE_INTERVAL_MS = 1000; // ~1 fps to the vision model const SAMPLE_INTERVAL_MS = 1000; // ~1 fps to the vision model
const SCREEN_THUMBNAIL_WIDTH = 360;
const SHUTDOWN_GRACE_MS = 5000; const SHUTDOWN_GRACE_MS = 5000;
async function agentToken(room: string): Promise<string> { async function agentToken(room: string): Promise<string> {
@@ -91,13 +92,23 @@ async function main() {
try { try {
const rgba = event.frame.convert(VideoBufferType.RGBA); const rgba = event.frame.convert(VideoBufferType.RGBA);
const jpeg = await sharp(Buffer.from(rgba.data), { const pixels = Buffer.from(rgba.data);
raw: { width: rgba.width, height: rgba.height, channels: 4 }, const raw = { width: rgba.width, height: rgba.height, channels: 4 } as const;
}) const [jpeg, thumbnail] = await Promise.all([
sharp(pixels, { raw })
.resize({ width: 1280, withoutEnlargement: true }) .resize({ width: 1280, withoutEnlargement: true })
.jpeg({ quality: 70 }) .jpeg({ quality: 70 })
.toBuffer(); .toBuffer(),
await podman.onScreenFrame(engineerId, jpeg); sharp(pixels, { raw })
.resize({ width: SCREEN_THUMBNAIL_WIDTH, withoutEnlargement: true })
.jpeg({ quality: 42 })
.toBuffer(),
]);
await podman.onScreenFrame(
engineerId,
jpeg,
`data:image/jpeg;base64,${thumbnail.toString('base64')}`,
);
} finally { } finally {
inFlight.delete(engineerId); inFlight.delete(engineerId);
} }
+2 -1
View File
@@ -46,8 +46,9 @@ export class PodMan {
}); });
} }
async onScreenFrame(engineerId: string, jpeg: Buffer): Promise<void> { async onScreenFrame(engineerId: string, jpeg: Buffer, screenshotDataUrl?: string): Promise<void> {
const ctx = await analyzeFrame(engineerId, this.podId, jpeg); const ctx = await analyzeFrame(engineerId, this.podId, jpeg);
if (screenshotDataUrl) ctx.screenshotDataUrl = screenshotDataUrl;
this.contexts.set(engineerId, ctx); this.contexts.set(engineerId, ctx);
await recordObservation(ctx); await recordObservation(ctx);
+15
View File
@@ -802,6 +802,16 @@ function ActivityItem({ event }: { event: PodActivityEvent }) {
{event.detail} {event.detail}
</p> </p>
)} )}
{event.imageUrl && (
<div className="mt-2 overflow-hidden rounded-md border bg-muted">
<img
src={event.imageUrl}
alt={activityImageAlt(event)}
loading="lazy"
className="aspect-video w-full object-cover"
/>
</div>
)}
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5"> <div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5">
<SourceChip source={event.source} /> <SourceChip source={event.source} />
<Badge variant="outline" className="rounded-md px-1.5 py-0 text-[0.68rem] leading-4"> <Badge variant="outline" className="rounded-md px-1.5 py-0 text-[0.68rem] leading-4">
@@ -960,6 +970,11 @@ function activityTitle(event: PodActivityEvent): string {
return event.title.startsWith('Screen') ? event.title : `Screen log: ${event.title}`; return event.title.startsWith('Screen') ? event.title : `Screen log: ${event.title}`;
} }
function activityImageAlt(event: PodActivityEvent): string {
const actor = event.actor ?? event.actors?.[0] ?? 'teammate';
return `${actor} screen thumbnail`;
}
function activityIcon(kind: PodActivityKind) { function activityIcon(kind: PodActivityKind) {
switch (kind) { switch (kind) {
case 'git': case 'git':
+1
View File
@@ -14,6 +14,7 @@ export interface PodActivityEvent {
actor?: string; actor?: string;
actors?: string[]; actors?: string[];
file?: string; file?: string;
imageUrl?: string;
severity: PodActivitySeverity; severity: PodActivitySeverity;
at: string; at: string;
} }
+2
View File
@@ -16,6 +16,8 @@ export interface EngineerContext {
hasUnpushedChanges?: boolean; hasUnpushedChanges?: boolean;
/** 01 confidence in this read of the screen. */ /** 01 confidence in this read of the screen. */
confidence: number; confidence: number;
/** Small redacted-size JPEG data URL for sidebar preview, not the full frame. */
screenshotDataUrl?: string;
/** ISO timestamp of the observation this context came from. */ /** ISO timestamp of the observation this context came from. */
observedAt: string; observedAt: string;
} }