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:
1 parent
a36a66ce6d
commit
c2c90a6378
84 files changed
+5612
-645
No files matched your search
@@ -77,6 +77,11 @@
|
||||
{{ getTaskStatus(session) | uppercase }}
|
||||
</span>
|
||||
<div class="task-card-header-actions">
|
||||
<span class="task-device" *ngIf="getDeviceSerial(session)" [title]="'Device: ' + getDeviceSerial(session)">
|
||||
<span class="material-symbols-outlined device-icon">phone_android</span>
|
||||
<span class="device-name">{{ getDeviceSerial(session) }}</span>
|
||||
</span>
|
||||
<span class="task-time-divider" *ngIf="getDeviceSerial(session)">·</span>
|
||||
<span class="task-time">{{ session.start_time * 1000 | date:'shortTime' }}</span>
|
||||
<button
|
||||
*ngIf="getTaskStatus(session) !== 'running' && getTaskStatus(session) !== 'paused'"
|
||||
@@ -126,6 +131,11 @@
|
||||
{{ getTaskStatus(session) | uppercase }}
|
||||
</span>
|
||||
<div class="task-card-header-actions">
|
||||
<span class="task-device" *ngIf="getDeviceSerial(session)" [title]="'Device: ' + getDeviceSerial(session)">
|
||||
<span class="material-symbols-outlined device-icon">phone_android</span>
|
||||
<span class="device-name">{{ getDeviceSerial(session) }}</span>
|
||||
</span>
|
||||
<span class="task-time-divider" *ngIf="getDeviceSerial(session)">·</span>
|
||||
<span class="task-time">{{ session.start_time * 1000 | date:'shortTime' }}</span>
|
||||
<button
|
||||
*ngIf="getTaskStatus(session) !== 'running' && getTaskStatus(session) !== 'paused'"
|
||||
|
||||
@@ -296,7 +296,8 @@ $text-muted: #a1a1aa;
|
||||
box-shadow: 0 3px 8px rgba(148, 163, 184, 0.08);
|
||||
|
||||
.task-card-header .task-card-header-actions {
|
||||
&:has(.btn-delete-card) .task-time {
|
||||
&:has(.btn-delete-card) .task-time,
|
||||
&:has(.btn-delete-card) .task-time-divider {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -327,8 +328,11 @@ $text-muted: #a1a1aa;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.task-badge {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 10.5px;
|
||||
@@ -405,11 +409,67 @@ $text-muted: #a1a1aa;
|
||||
.task-card-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
justify-content: flex-end;
|
||||
|
||||
.task-device {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2.5px;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
cursor: default;
|
||||
transition: color 0.15s ease, opacity 0.15s ease;
|
||||
|
||||
.device-icon {
|
||||
font-size: 12.5px;
|
||||
width: 12.5px;
|
||||
height: 12.5px;
|
||||
line-height: 12.5px;
|
||||
color: #94a3b8;
|
||||
opacity: 0.75;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
font-family: inherit;
|
||||
letter-spacing: -0.15px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #64748b;
|
||||
.device-icon {
|
||||
color: #64748b;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-time-divider {
|
||||
font-size: 10px;
|
||||
color: #cbd5e1;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.task-time {
|
||||
font-size: 11.5px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,9 +49,11 @@ export class ChatInterfaceComponent {
|
||||
return list.sort((a, b) => {
|
||||
const statusA = this.getTaskStatus(a);
|
||||
const statusB = this.getTaskStatus(b);
|
||||
if (statusA === 'running' || statusA === 'paused') return -1;
|
||||
if (statusB === 'running' || statusB === 'paused') return 1;
|
||||
return a.start_time - b.start_time; // FIFO order
|
||||
const isRunA = statusA === 'running' || statusA === 'paused';
|
||||
const isRunB = statusB === 'running' || statusB === 'paused';
|
||||
if (isRunA && !isRunB) return -1;
|
||||
if (!isRunA && isRunB) return 1;
|
||||
return (a.start_time || 0) - (b.start_time || 0); // stable FIFO order
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,18 +157,43 @@ export class ChatInterfaceComponent {
|
||||
* Determine the current task execution status
|
||||
*/
|
||||
public getTaskStatus(session: Session): 'running' | 'paused' | 'completed' | 'pending' | 'failed' | 'cancelled' {
|
||||
if (session.session_id === this.agentService.runningSessionId() && (this.agentService.agentStatus() === 'running' || this.agentService.agentStatus() === 'paused')) {
|
||||
return this.agentService.agentStatus() as 'running' | 'paused';
|
||||
}
|
||||
if (session.status) {
|
||||
const s = session.status.toLowerCase();
|
||||
if (s === 'running' || s === 'paused' || s === 'completed' || s === 'pending' || s === 'failed' || s === 'cancelled') {
|
||||
if (s === 'completed' || s === 'success' || s === 'failed' || s === 'cancelled') {
|
||||
return (s === 'success' ? 'completed' : s) as any;
|
||||
}
|
||||
if (s === 'running' || s === 'paused' || s === 'pending') {
|
||||
return s as any;
|
||||
}
|
||||
}
|
||||
if (session.session_id === this.agentService.runningSessionId() && (this.agentService.agentStatus() === 'running' || this.agentService.agentStatus() === 'paused')) {
|
||||
return this.agentService.agentStatus() as 'running' | 'paused';
|
||||
}
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the device serial number for the session
|
||||
*/
|
||||
public getDeviceSerial(session: Session): string | null {
|
||||
const serial = session.device_serial || session.device_id;
|
||||
if (serial && serial !== 'pending' && serial !== 'null' && serial !== 'undefined') {
|
||||
return serial;
|
||||
}
|
||||
if (session.device_info) {
|
||||
try {
|
||||
const info = typeof session.device_info === 'string' ? JSON.parse(session.device_info) : session.device_info;
|
||||
const s = info?.device_id || info?.device_serial;
|
||||
if (s && s !== 'pending' && s !== 'null' && s !== 'undefined') {
|
||||
return s;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a session in the UI to monitor its steps
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user