feat: add multi-device concurrency, server lifecycle CLI, and unified execution stream

- Implement DevicePool and DeviceExecutionLock concurrency modes (per-device FIFO vs global serialization)
- Extend ArtemisClient SDK and CLI with explicit device targeting and concurrency strategy configuration
- Add server lifecycle management (artemis restart, stop, status) with cross-platform PID discovery
- Update MCP server tools to support multi-device dispatch, trace tagging, and per-device execution
- Enhance Showcase UI with step-by-step visual replay, playback speed controls, and device serial badges
- Add comprehensive unit test coverage and edge-case verification scripts
This commit is contained in:
somew1nd committed 2026-08-26 15:53:14 -07:00
1 parent a36a66ce6d
commit c2c90a6378
84 files changed
+5612 -645

No files matched your search

@@ -29,6 +29,8 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AgentService } from '../../services/agent.service';
import { StepReplayFrame } from '../../core/models/stream.model';
import { drawActionCoordinatesOnOverlay } from '../../utils/image-overlay.util';
import { getActionIcon } from '../../utils/action-formatter.util';
@Component({
selector: 'app-floating-video-player',
@@ -41,6 +43,8 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
public agentService = inject(AgentService);
@ViewChild('videoRef') videoRef?: ElementRef<HTMLVideoElement>;
@ViewChild('stepImgRef') stepImgRef?: ElementRef<HTMLImageElement>;
@ViewChild('stepOverlayRef') stepOverlayRef?: ElementRef<HTMLElement>;
// Playback state signals
public isPlaying = signal<boolean>(false);
@@ -65,6 +69,12 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
// Step Replay state signals
public activeStepIndex = signal<number>(0);
public isStepPlaying = signal<boolean>(false);
public viewMode = signal<'pre' | 'post'>('pre'); // 'pre' = action preview with coords, 'post' = action result
public isCardHovered = signal<boolean>(false);
public isCardPinned = signal<boolean>(false);
public isCardVisible = computed(() => this.isCardHovered() || this.isCardPinned());
public isStepImageLoading = signal<boolean>(false);
public stepImageError = signal<boolean>(false);
private stepTimer: any = null;
public stepFrames = computed(() => this.agentService.currentSessionStepFrames());
@@ -74,6 +84,14 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
const idx = this.activeStepIndex();
return frames[idx] || null;
});
public currentStepImageUrl = computed<string>(() => {
const frame = this.currentStepFrame();
if (!frame) return '';
if (this.viewMode() === 'post' && frame.postImageUrl) {
return frame.postImageUrl;
}
return frame.preImageUrl || frame.imageUrl || '';
});
public stepProgressPercent = computed(() => {
const total = this.totalStepFrames();
if (total <= 1) return 0;
@@ -85,10 +103,10 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
// Dragging and window positioning state
public posX = signal<number>(
typeof window !== 'undefined' ? Math.max(20, window.innerWidth - 380) : 100
typeof window !== 'undefined' ? Math.max(20, window.innerWidth - 420) : 100
);
public posY = signal<number>(
typeof window !== 'undefined' ? Math.max(60, window.innerHeight - 660) : 100
typeof window !== 'undefined' ? Math.max(60, window.innerHeight - 700) : 100
);
private isDragging = false;
@@ -117,11 +135,30 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
() => {
const _ = this.stepFrames();
this.activeStepIndex.set(0);
this.viewMode.set('pre');
this.pauseStepPlay();
},
{ allowSignalWrites: true }
);
// Track step screenshot URL changes and reset loading/error state
effect(() => {
const url = this.currentStepImageUrl();
if (url) {
this.isStepImageLoading.set(true);
this.stepImageError.set(false);
} else {
this.isStepImageLoading.set(false);
}
}, { allowSignalWrites: true });
// Re-draw overlay whenever activeStepIndex or viewMode changes
effect(() => {
this.activeStepIndex();
this.viewMode();
setTimeout(() => this.drawStepOverlay(), 40);
});
effect(() => {
const request = this.agentService.videoSeekRequest();
if (!request) return;
@@ -444,15 +481,18 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
public prevStepFrame(): void {
this.pauseStepPlay();
this.viewMode.set('pre');
this.activeStepIndex.update((i) => Math.max(0, i - 1));
}
public nextStepFrame(): void {
this.pauseStepPlay();
this.viewMode.set('pre');
this.activeStepIndex.update((i) => Math.min(this.totalStepFrames() - 1, i + 1));
}
public seekStepFrame(index: number): void {
this.viewMode.set('pre');
this.activeStepIndex.set(Math.max(0, Math.min(this.totalStepFrames() - 1, index)));
}
@@ -466,8 +506,75 @@ export class FloatingVideoPlayerComponent implements OnDestroy {
this.startStepPlay();
}
public setStepViewMode(mode: 'pre' | 'post', event?: Event): void {
if (event) event.stopPropagation();
this.viewMode.set(mode);
setTimeout(() => this.drawStepOverlay(), 40);
}
public toggleStepViewMode(event?: Event): void {
if (event) event.stopPropagation();
this.setStepViewMode(this.viewMode() === 'pre' ? 'post' : 'pre');
}
public onViewportMouseEnter(): void {
this.isCardHovered.set(true);
}
public onViewportMouseLeave(): void {
this.isCardHovered.set(false);
}
public toggleCardPin(event?: Event): void {
if (event) event.stopPropagation();
this.isCardPinned.update((v) => !v);
}
public cycleSpeed(): void {
const current = this.playbackRate();
const idx = this.speedOptions.indexOf(current);
const nextIdx = (idx + 1) % this.speedOptions.length;
this.setSpeed(this.speedOptions[nextIdx]);
}
public onStepImageLoad(): void {
this.isStepImageLoading.set(false);
this.stepImageError.set(false);
this.drawStepOverlay();
}
public onStepImageError(): void {
this.isStepImageLoading.set(false);
this.stepImageError.set(true);
}
public drawStepOverlay(): void {
const img = this.stepImgRef?.nativeElement;
const overlay = this.stepOverlayRef?.nativeElement;
if (!img || !overlay) return;
overlay.innerHTML = '';
// Only draw coordinate overlays when viewing the pre-action screenshot
if (this.viewMode() === 'post') {
return;
}
const frame = this.currentStepFrame();
if (!frame || !frame.action) return;
try {
drawActionCoordinatesOnOverlay(img, overlay, frame.action);
} catch (err) {
console.warn('Failed to draw step coordinates overlay:', err);
}
}
public getStepActionIcon(action: any): string {
return getActionIcon(action);
}
public openImageInNewTab(): void {
const url = this.currentStepFrame()?.imageUrl;
const url = this.currentStepImageUrl() || this.currentStepFrame()?.imageUrl;
if (url) {
window.open(url, '_blank');
}