feat: introduce MCP server, cross-platform runtime, and Showcase UI reorganization

- Showcase UI Reorganization:
  * Move web UI dashboard from root directory into apps/showcase_ui/ for cleaner project structure
  * Add 'artemis ui' CLI command (artemis/interfaces/cli/commands/ui.py) and launcher scripts for the interactive showcase console
- Standalone MCP Server & Notifiers (mcp_server/):
  * Implement dedicated MCP server package with mobile automation tools (device_state, inspect_trace, task_manager, task_runner)
  * Add multi-channel notifiers (AgentAPI, Desktop notifications, Webhooks, File output, and Composite routing)
  * Introduce background task runner and trace store for autonomous mobile execution monitoring
- Cross-Platform Runtime & Toolchain Support:
  * Add artemis/platform/ with OS-specific implementations for Linux, macOS (Darwin), and Windows
  * Add artemis/runtime/supervisor.py for resilient process and agent runtime supervision
  * Add artemis/toolchain/ with dependency resolution and SDK descriptors
  * Add cross-platform launcher scripts (start.sh, start.bat) and installer scripts (scripts/install_deps.*)
- Core Enhancements & Diagnostics:
  * Enhance coordinate normalization, OCR API fallback, and perception pipeline
  * Add credential validation (credentials_validator.py) and system diagnostics
  * Update bilingual documentation (README.md, README_CN.md) and architecture diagrams
- Test Suite Expansion:
  * Add integration tests for cloud gateway, remote ADB, and Artemis login
  * Add unit tests covering core diagnostics, platform detection, toolchain, credentials validator, optional OCR, and MCP tools
This commit is contained in:
somew1nd committed 2026-08-18 13:59:27 -07:00
1 parent 78e478f502
commit 9a5aaf99b7
249 files changed
+37226 -9339

No files matched your search

@@ -0,0 +1,116 @@
<!--
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="agent-stream-container">
<!-- Header with Session Selector -->
<header class="stream-header">
<div class="header-title">
<h2>👣 Real-time Agent Stream (Legacy Card View)</h2>
<p class="subtitle">Select a session to monitor raw agent activity</p>
</div>
<div class="session-selector-wrapper">
<label for="session-select">Active Session:</label>
<select
id="session-select"
[value]="agentService.currentSessionId() || ''"
(change)="onSessionChange($event)"
class="session-dropdown"
>
<option value="" disabled selected>-- Select a Session --</option>
<option
*ngFor="let session of agentService.sessions()"
[value]="session.session_id"
>
{{ session.initial_goal || 'Unnamed Goal' }} ({{ session.start_time * 1000 | date:'short' }})
</option>
</select>
<button class="btn-refresh" (click)="agentService.fetchSessions()">🔄 Refresh</button>
</div>
</header>
<!-- Event Logs Section -->
<main class="stream-logs-content">
<div *ngIf="agentService.sessionLogs().length === 0" class="empty-state">
<div class="empty-icon">📡</div>
<h3>No Stream Activity Selected</h3>
<p>Please select a running session from the dropdown above to view real-time log payloads.</p>
</div>
<div *ngIf="agentService.sessionLogs().length > 0" class="logs-list">
<div
*ngFor="let log of agentService.sessionLogs(); let i = index"
class="log-card"
[ngClass]="['type-' + log.type, isStepEvent(log.type) ? 'step-event-card' : 'raw-event-card']"
>
<div class="log-card-header">
<div class="badge-group">
<span class="index-badge">#{{ i + 1 }}</span>
<span class="type-badge" [ngClass]="log.type">{{ log.type }}</span>
</div>
<span class="timestamp">{{ log.timestamp | date:'HH:mm:ss.SSS' }}</span>
</div>
<!-- Render Step Events in a beautiful, structured layout -->
<div *ngIf="isStepEvent(log.type)" class="log-card-body step-body">
<!-- Step Summary (Message to User) -->
<div class="step-summary-section" *ngIf="log.data.summary">
<div class="section-title">💬 Message to User / Summary:</div>
<div class="summary-bubble">{{ log.data.summary }}</div>
</div>
<!-- Model thinking process (collapsible) -->
<details class="thinking-details" *ngIf="log.data.operator_native_thinking">
<summary class="details-summary">🧠 Model Thinking Process (Click to expand)</summary>
<div class="details-content thinking-text">{{ log.data.operator_native_thinking }}</div>
</details>
<!-- Raw thinking process (collapsible) -->
<details class="planning-details" *ngIf="log.data.operator_raw_thinking">
<summary class="details-summary">📝 Raw Plan / Thoughts (Click to expand)</summary>
<div class="details-content planning-text">{{ log.data.operator_raw_thinking }}</div>
</details>
<!-- Tool Call (collapsible) -->
<details class="tool-details" *ngIf="log.data.action_taken" [open]="isShortContent(log.data.action_taken)">
<summary class="details-summary">🛠️ Tool Call Details (Click to expand)</summary>
<div class="details-content">
<pre class="json-code"><code>{{ formatJson(log.data.action_taken) }}</code></pre>
</div>
</details>
<!-- Tool Call Result (collapsible) -->
<details class="result-details" *ngIf="log.data.last_execution_result">
<summary class="details-summary">✓ Execution Result (Click to expand)</summary>
<div class="details-content">
<pre class="json-code"><code>{{ formatJson(log.data.last_execution_result) }}</code></pre>
</div>
</details>
</div>
<!-- Render other event types as raw JSON (collapsible) -->
<div *ngIf="!isStepEvent(log.type)" class="log-card-body raw-body">
<details class="raw-details" [open]="isShortContent(log.data)">
<summary class="details-summary">📄 Raw Payload Data (Click to expand)</summary>
<div class="details-content">
<pre class="json-code"><code>{{ formatJson(log.data) }}</code></pre>
</div>
</details>
</div>
</div>
</div>
</main>
</div>
@@ -0,0 +1,406 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$border-color: rgba(0, 0, 0, 0.08);
$text-primary: #1f2937;
$text-secondary: #4b5563;
$text-muted: #9ca3af;
$bg-main: #f9fafb;
:host {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
}
.agent-stream-container {
display: flex;
flex-direction: column;
height: 100%;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(16px);
box-sizing: border-box;
.stream-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid $border-color;
background: rgba(255, 255, 255, 0.6);
flex-wrap: wrap;
gap: 16px;
.header-title {
h2 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 700;
color: $text-primary;
letter-spacing: -0.5px;
}
.subtitle {
margin: 0;
font-size: 13px;
color: $text-secondary;
}
}
.session-selector-wrapper {
display: flex;
align-items: center;
gap: 12px;
label {
font-size: 13.5px;
font-weight: 600;
color: $text-primary;
}
.session-dropdown {
padding: 8px 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px;
font-size: 13.5px;
background-color: #ffffff;
outline: none;
color: $text-primary;
min-width: 250px;
max-width: 400px;
cursor: pointer;
transition: border-color 0.2s;
&:focus {
border-color: #3b82f6;
}
}
.btn-refresh {
padding: 8px 14px;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
color: $text-primary;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background 0.2s, transform 0.1s;
&:hover {
background: #f3f4f6;
}
&:active {
transform: scale(0.98);
}
}
}
}
.stream-logs-content {
flex: 1;
overflow-y: auto;
padding: 24px;
background: rgba(249, 250, 251, 0.3);
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 10px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
.empty-state {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 40px;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.8;
}
h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: $text-primary;
}
p {
margin: 0;
font-size: 14px;
color: $text-secondary;
max-width: 320px;
line-height: 1.5;
}
}
.logs-list {
display: flex;
flex-direction: column;
gap: 16px;
.log-card {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.01), 0 2px 4px -1px rgba(0, 0, 0, 0.006);
display: flex;
flex-direction: column;
gap: 12px;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.02), 0 4px 6px -2px rgba(0, 0, 0, 0.01);
}
.log-card-header {
display: flex;
justify-content: space-between;
align-items: center;
.badge-group {
display: flex;
align-items: center;
gap: 8px;
.index-badge {
font-size: 11px;
font-weight: 700;
color: $text-muted;
font-family: monospace;
}
.type-badge {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
padding: 2.5px 7px;
border-radius: 9999px;
letter-spacing: 0.5px;
font-family: monospace;
&.llm_stream {
background: #e0f2fe;
color: #0369a1;
}
&.trace_recorded {
background: #f3e8ff;
color: #6b21a8;
}
&.step_recorded {
background: #ecfdf5;
color: #047857;
}
&.step_updated {
background: #fef3c7;
color: #b45309;
}
&.background_tasks_updated {
background: #fff1f2;
color: #be123c;
}
}
}
.timestamp {
font-size: 11.5px;
color: $text-muted;
font-family: monospace;
}
}
.log-card-body {
&.step-body {
display: flex;
flex-direction: column;
gap: 16px;
.step-summary-section {
.section-title {
font-size: 11px;
font-weight: 700;
color: $text-secondary;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-bubble {
background: #f1f5f9;
border: 1px solid rgba(0, 0, 0, 0.05);
border-left: 4px solid #475569;
border-radius: 8px;
padding: 12px 16px;
font-size: 13.5px;
line-height: 1.5;
color: #1e293b;
}
}
details {
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
background: #fafafa;
overflow: hidden;
transition: border-color 0.2s;
&[open] {
border-color: rgba(0, 0, 0, 0.12);
.details-summary {
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.02);
}
}
.details-summary {
padding: 10px 14px;
font-size: 12.5px;
font-weight: 700;
color: $text-secondary;
cursor: pointer;
user-select: none;
outline: none;
background: #ffffff;
transition: background 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.01);
}
}
.details-content {
padding: 14px;
font-size: 13px;
line-height: 1.6;
color: #334155;
&.thinking-text {
background: #f0fdf4;
color: #14532d;
white-space: pre-wrap;
font-family: inherit;
}
&.planning-text {
background: #f8fafc;
color: #334155;
white-space: pre-wrap;
font-family: inherit;
}
}
&.thinking-details {
border-left: 4px solid #16a34a;
.details-summary {
color: #16a34a;
}
}
&.planning-details {
border-left: 4px solid #4f46e5;
.details-summary {
color: #4f46e5;
}
}
&.tool-details {
border-left: 4px solid #2563eb;
.details-summary {
color: #2563eb;
}
}
&.result-details {
border-left: 4px solid #0891b2;
.details-summary {
color: #0891b2;
}
}
}
}
&.raw-body {
details {
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
overflow: hidden;
.details-summary {
padding: 10px 14px;
font-size: 12px;
font-weight: 600;
color: $text-secondary;
cursor: pointer;
outline: none;
background: #ffffff;
}
.details-content {
padding: 12px;
}
}
}
.json-code {
margin: 0;
background: #1e293b;
color: #f8fafc;
padding: 14px 18px;
border-radius: 8px;
font-family: 'Fira Code', 'Roboto Mono', monospace;
font-size: 12.5px;
line-height: 1.5;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}
}
}
}
}
@@ -0,0 +1,72 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AgentService } from '../../services/agent.service';
@Component({
selector: 'app-legacy-agent-stream',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './legacy-agent-stream.component.html',
styleUrl: './legacy-agent-stream.component.scss'
})
export class LegacyAgentStreamComponent {
public agentService = inject(AgentService);
/**
* Handle dropdown session selection change
*/
public onSessionChange(event: Event): void {
const selectElement = event.target as HTMLSelectElement;
const sessionId = selectElement.value;
if (sessionId) {
this.agentService.selectSession(sessionId, true);
}
}
/**
* Check if the event type represents a structured step record
*/
public isStepEvent(eventType: string): boolean {
return eventType === 'step_recorded' || eventType === 'step_updated';
}
/**
* Determine if the content is short enough to remain open by default
*/
public isShortContent(data: any): boolean {
if (!data) return true;
const str = typeof data === 'string' ? data : JSON.stringify(data);
return str.length < 200;
}
/**
* Format JSON data for display in HTML pre tag
*/
public formatJson(data: any): string {
if (typeof data === 'string') {
try {
return JSON.stringify(JSON.parse(data), null, 2);
} catch (e) {
return data;
}
}
return JSON.stringify(data, null, 2);
}
}