Watch
1
0
Fork
You've already forked RedFlag
0

fix: screenshot square inside System Information card; display discovery

UI: move screenshot/Sunshine square into the System Information card
header (top-right, w-48 aspect-video) instead of a standalone block
above the card. Same click logic, smaller size to fit the header row.

Agent: captureScreenLinux now discovers DISPLAY/WAYLAND_DISPLAY/
XDG_RUNTIME_DIR from /proc environ entries so the service (which
doesn't inherit display vars from systemd) can reach the session.
Tool priority: scrot → grim → magick import → import.
Adds bytes/strconv/strings imports for discoverSessionDisplayEnv.
This commit is contained in:
Fimeg 2026-06-08 18:02:33 -04:00
commit 80e719acc9
2 changed files with 156 additions and 119 deletions

View file

@ -816,97 +816,84 @@ const Agents: React.FC = () => {
)}
</div>
{/* Screen square — screenshot capture / Sunshine connection link */}
{(() => {
const integrations = readIntegrations(selectedAgent.metadata);
const sunshine = integrations.sunshine;
const state = resolveState(sunshine);
const live = state === 'active';
const running = state === 'running';
const sunshineReady = live || running;
const isCapturing = captureScreenshotMutation.isPending;
const isPolling = screenshotCommand && !['completed', 'failed', 'timed_out', 'cancelled'].includes(screenshotCommand.status);
// Determine what the square shows
const hasImage = !!screenshotImage;
const squareClickable = sunshineReady || !isCapturing;
return (
<div className="mb-6">
<div
className={cn(
'relative aspect-video w-full max-w-sm overflow-hidden rounded border',
'bg-gradient-to-br from-slate-800 to-slate-900 border-slate-700',
'flex flex-col items-center justify-center gap-2 text-slate-400',
squareClickable && 'cursor-pointer hover:from-slate-700 hover:to-slate-800 transition-colors'
)}
onClick={() => {
if (sunshineReady && sunshine?.web_ui) {
window.open(sunshine.web_ui, '_blank', 'noreferrer');
} else if (!isCapturing && !isPolling) {
handleCaptureScreenshot(selectedAgent.id);
}
}}
>
{hasImage ? (
<img
src={`data:image/png;base64,${screenshotImage}`}
alt="Agent screenshot"
className="absolute inset-0 w-full h-full object-contain"
/>
) : (
<>
<MonitorPlay className="h-10 w-10 opacity-70" />
<span className="text-xs">
{isCapturing ? 'Requesting...'
: isPolling ? 'Capturing...'
: sunshineReady ? 'Open stream host'
: 'Click to capture'}
</span>
</>
)}
{/* LIVE marker when Sunshine session is active */}
{live && (
<span className="absolute top-2 left-2 flex items-center gap-1 rounded bg-red-600/90 px-1.5 py-0.5 text-[10px] font-medium text-white">
<span className="h-1.5 w-1.5 rounded-full bg-white animate-pulse" />
LIVE
</span>
)}
{/* Sunshine status badge */}
{sunshineReady && (
<span className="absolute top-2 right-2 rounded bg-black/50 px-1.5 py-0.5 text-[10px] font-medium text-white">
{live ? sunshine?.client_name || 'Connected' : 'Sunshine running'}
</span>
)}
{/* Loading spinner overlay */}
{(isCapturing || isPolling) && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<RefreshCw className="h-8 w-8 text-white animate-spin" />
</div>
)}
</div>
{/* Caption row beneath the square */}
<div className="flex items-center gap-2 mt-1.5 text-xs text-gray-500">
{sunshine?.version && <span>Sunshine v{sunshine.version}</span>}
{screenshotCommand?.status === 'completed' && screenshotCommand?.completed_at && (
<span className="ml-auto">Captured {formatRelativeTime(screenshotCommand.completed_at)}</span>
)}
{screenshotCommand?.status === 'failed' && (
<span className="text-red-500 ml-auto">Capture failed</span>
)}
</div>
</div>
);
})()}
{/* System info */}
{/* System info — screenshot square lives in the card header */}
<div className="card">
<h2 className="text-lg font-medium text-gray-900 mb-4">System Information</h2>
{(() => {
const integrations = readIntegrations(selectedAgent.metadata);
const sunshine = integrations.sunshine;
const state = resolveState(sunshine);
const live = state === 'active';
const sunshineReady = live || state === 'running';
const isCapturing = captureScreenshotMutation.isPending;
const isPolling = screenshotCommand && !['completed', 'failed', 'timed_out', 'cancelled'].includes(screenshotCommand.status);
const hasImage = !!screenshotImage;
const canClick = !isCapturing && !isPolling;
return (
<div className="flex items-start justify-between mb-4 gap-4">
<h2 className="text-lg font-medium text-gray-900">System Information</h2>
<div className="shrink-0 w-48">
<div
className={cn(
'relative aspect-video w-full overflow-hidden rounded border',
'bg-gradient-to-br from-slate-800 to-slate-900 border-slate-700',
'flex flex-col items-center justify-center gap-1 text-slate-400',
canClick && 'cursor-pointer hover:from-slate-700 hover:to-slate-800 transition-colors'
)}
onClick={() => {
if (sunshineReady && sunshine?.web_ui) {
window.open(sunshine.web_ui, '_blank', 'noreferrer');
} else if (canClick) {
handleCaptureScreenshot(selectedAgent.id);
}
}}
>
{hasImage ? (
<img
src={`data:image/png;base64,${screenshotImage}`}
alt="Agent screenshot"
className="absolute inset-0 w-full h-full object-contain"
/>
) : (
<>
<MonitorPlay className="h-6 w-6 opacity-70" />
<span className="text-[10px]">
{isCapturing ? 'Requesting…'
: isPolling ? 'Capturing…'
: sunshineReady ? 'Open stream'
: 'Click to capture'}
</span>
</>
)}
{live && (
<span className="absolute top-1 left-1 flex items-center gap-1 rounded bg-red-600/90 px-1 py-0.5 text-[9px] font-medium text-white">
<span className="h-1.5 w-1.5 rounded-full bg-white animate-pulse" />
LIVE
</span>
)}
{sunshineReady && !live && (
<span className="absolute top-1 right-1 rounded bg-black/50 px-1 py-0.5 text-[9px] text-white">
Sunshine
</span>
)}
{(isCapturing || isPolling) && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<RefreshCw className="h-5 w-5 text-white animate-spin" />
</div>
)}
</div>
<div className="flex items-center justify-between mt-1 text-[10px] text-gray-500">
{sunshine?.version ? <span>Sunshine v{sunshine.version}</span> : <span />}
{screenshotCommand?.status === 'completed' && screenshotCommand?.completed_at
? <span>{formatRelativeTime(screenshotCommand.completed_at)}</span>
: screenshotCommand?.status === 'failed'
? <span className="text-red-500">Failed</span>
: null}
</div>
</div>
</div>
);
})()}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic System Info */}