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:
1 parent
78e478f502
commit
9a5aaf99b7
249 files changed
+37226
-9339
No files matched your search
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 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 { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote } from '../core/models/markdown.model';
|
||||
import { CheckerResult } from '../core/models/stream.model';
|
||||
|
||||
/**
|
||||
* Split text into bold, code, and plain segments
|
||||
*/
|
||||
export function parseMarkdownSegments(text: string): MarkdownSegment[] {
|
||||
if (!text) return [];
|
||||
const parts = text.split(/(\*\*.*?\*\*|`.*?`)/g);
|
||||
return parts
|
||||
.filter(part => part !== '')
|
||||
.map(part => {
|
||||
if (part.startsWith('**') && part.endsWith('**')) {
|
||||
return {
|
||||
text: part.slice(2, -2),
|
||||
bold: true,
|
||||
code: false
|
||||
};
|
||||
} else if (part.startsWith('`') && part.endsWith('`')) {
|
||||
return {
|
||||
text: part.slice(1, -1),
|
||||
bold: false,
|
||||
code: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: part,
|
||||
bold: false,
|
||||
code: false
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse markdown note content into structured lines (checklists, headers, list items)
|
||||
*/
|
||||
export function parseNoteLines(content: string): MarkdownLine[] {
|
||||
if (!content) return [];
|
||||
return content.split('\n').map(line => {
|
||||
const trimmed = line.trim();
|
||||
const indent = line.length - line.trimStart().length;
|
||||
|
||||
// Checkbox [x]
|
||||
if (trimmed.startsWith('- [x] ') || trimmed.startsWith('- [X] ')) {
|
||||
const rawText = trimmed.substring(6);
|
||||
return { type: 'checked', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
// Active checkbox [/]
|
||||
if (trimmed.startsWith('- [/] ')) {
|
||||
const rawText = trimmed.substring(6);
|
||||
return { type: 'progress', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
// Unchecked checkbox [ ]
|
||||
if (trimmed.startsWith('- [ ] ')) {
|
||||
const rawText = trimmed.substring(6);
|
||||
return { type: 'unchecked', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
// Regular list item
|
||||
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
||||
const rawText = trimmed.substring(2);
|
||||
return { type: 'list-item', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
// Headers
|
||||
if (trimmed.startsWith('# ')) {
|
||||
const rawText = trimmed.substring(2);
|
||||
return { type: 'h1', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
if (trimmed.startsWith('## ')) {
|
||||
const rawText = trimmed.substring(3);
|
||||
return { type: 'h2', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
if (trimmed.startsWith('### ')) {
|
||||
const rawText = trimmed.substring(4);
|
||||
return { type: 'h3', segments: parseMarkdownSegments(rawText), indent };
|
||||
}
|
||||
// Default line
|
||||
return trimmed ? { type: 'text', segments: parseMarkdownSegments(line), indent } : { type: 'empty', segments: [], indent: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Group parsed markdown lines into title, milestones (checklists), and other lines
|
||||
*/
|
||||
export function parseNote(content: string): ParsedNote {
|
||||
const lines = parseNoteLines(content);
|
||||
const milestones: NoteMilestone[] = [];
|
||||
const otherLines: MarkdownLine[] = [];
|
||||
let title: string | null = null;
|
||||
let currentMilestone: (NoteMilestone & { _indent?: number }) | null = null;
|
||||
let milestoneCount = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.type === 'empty') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((line.type === 'h1' || line.type === 'h2') && !title) {
|
||||
title = line.segments.map(s => s.text).join('');
|
||||
continue;
|
||||
}
|
||||
|
||||
const isChecklistItem = ['checked', 'progress', 'unchecked'].includes(line.type);
|
||||
|
||||
if (isChecklistItem && (!currentMilestone || line.indent <= (currentMilestone._indent ?? 0))) {
|
||||
milestoneCount++;
|
||||
currentMilestone = {
|
||||
index: milestoneCount,
|
||||
type: line.type as 'checked' | 'progress' | 'unchecked',
|
||||
segments: line.segments,
|
||||
subSteps: [],
|
||||
_indent: line.indent
|
||||
};
|
||||
milestones.push(currentMilestone);
|
||||
} else if (currentMilestone) {
|
||||
if (line.indent > (currentMilestone._indent ?? 0)) {
|
||||
currentMilestone.subSteps.push(line);
|
||||
} else {
|
||||
otherLines.push(line);
|
||||
}
|
||||
} else {
|
||||
otherLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
milestones,
|
||||
otherLines
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the Checker JSON response block
|
||||
*/
|
||||
export function extractCheckerResult(text: string): CheckerResult | null {
|
||||
if (!text) return null;
|
||||
let cleanText = text.trim();
|
||||
if (cleanText.includes('```json')) {
|
||||
const parts = cleanText.split('```json');
|
||||
if (parts.length > 1) {
|
||||
cleanText = parts[1].split('```')[0].trim();
|
||||
}
|
||||
} else if (cleanText.includes('```')) {
|
||||
const parts = cleanText.split('```');
|
||||
if (parts.length > 1) {
|
||||
cleanText = parts[1].split('```')[0].trim();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(cleanText);
|
||||
if (parsed && typeof parsed === 'object' && 'success' in parsed) {
|
||||
return {
|
||||
success: Boolean(parsed.success),
|
||||
reason: parsed.reason || 'No reason provided.'
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format inline markdown segments (bold, italic, code, strikethrough)
|
||||
*/
|
||||
function formatInlineMarkdown(text: string): string {
|
||||
if (!text) return '';
|
||||
return text
|
||||
// Inline code first (so asterisks inside code aren't formatted)
|
||||
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||||
// Bold + Italic
|
||||
.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>')
|
||||
.replace(/___([^_]+)___/g, '<strong><em>$1</em></strong>')
|
||||
// Bold
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/__([^_]+)__/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
.replace(/\b_([^_]+)_\b/g, '<em>$1</em>')
|
||||
// Strikethrough
|
||||
.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert basic markdown text to safe HTML for agent logs/thinking
|
||||
*/
|
||||
export function renderMarkdownToHtml(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
// Clean up internal agent XML-like tags (e.g. <thought>, <short_term_memory>, etc.)
|
||||
const cleanedText = text
|
||||
.replace(/<\/?(thought|short_term_memory|strong_term_memory|reasoning|plan|task_plan|call_tool)[^\n>]*>?/gi, '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
|
||||
// Escape HTML first to prevent XSS
|
||||
const escaped = cleanedText
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
const rawLines = escaped.split('\n');
|
||||
const result: string[] = [];
|
||||
let currentListType: 'ul' | 'ol' | null = null;
|
||||
let inCodeBlock = false;
|
||||
let codeBlockLang = '';
|
||||
let codeBlockLines: string[] = [];
|
||||
|
||||
const closeList = () => {
|
||||
if (currentListType) {
|
||||
result.push(`</${currentListType}>`);
|
||||
currentListType = null;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < rawLines.length; i++) {
|
||||
const rawLine = rawLines[i];
|
||||
const trimmed = rawLine.trim();
|
||||
|
||||
// 1. Code blocks: ```lang ... ```
|
||||
if (trimmed.startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
// End code block
|
||||
const codeContent = codeBlockLines.join('\n');
|
||||
result.push(`<pre class="code-block"><code class="lang-${codeBlockLang}">${codeContent}</code></pre>`);
|
||||
inCodeBlock = false;
|
||||
codeBlockLines = [];
|
||||
codeBlockLang = '';
|
||||
} else {
|
||||
closeList();
|
||||
inCodeBlock = true;
|
||||
codeBlockLang = trimmed.substring(3).trim();
|
||||
codeBlockLines = [];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeBlockLines.push(rawLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Headings: # to ######
|
||||
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
||||
if (headingMatch) {
|
||||
closeList();
|
||||
const level = headingMatch[1].length;
|
||||
const headingContent = formatInlineMarkdown(headingMatch[2]);
|
||||
result.push(`<h${level}>${headingContent}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Horizontal Rule: ---, ***, ___
|
||||
if (/^(\-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
||||
closeList();
|
||||
result.push('<hr class="md-hr" />');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Blockquotes: > quote
|
||||
if (trimmed.startsWith('> ') || trimmed.startsWith('>')) {
|
||||
closeList();
|
||||
const quoteContent = formatInlineMarkdown(trimmed.replace(/^>\s?/, ''));
|
||||
result.push(`<blockquote>${quoteContent}</blockquote>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Unordered List: - item or * item
|
||||
const ulMatch = rawLine.match(/^(\s*)([\*\-])\s+(.*)$/);
|
||||
if (ulMatch) {
|
||||
if (currentListType !== 'ul') {
|
||||
closeList();
|
||||
currentListType = 'ul';
|
||||
result.push('<ul>');
|
||||
}
|
||||
const spaces = ulMatch[1].length;
|
||||
const subLevel = Math.max(0, Math.floor((spaces - 2) / 2));
|
||||
const indent = subLevel * 12;
|
||||
const style = indent ? ` style="margin-left: ${indent}px"` : '';
|
||||
const itemContent = formatInlineMarkdown(ulMatch[3]);
|
||||
result.push(`<li${style}>${itemContent}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 6. Ordered List: 1. item
|
||||
const olMatch = rawLine.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
||||
if (olMatch) {
|
||||
if (currentListType !== 'ol') {
|
||||
closeList();
|
||||
currentListType = 'ol';
|
||||
result.push('<ol>');
|
||||
}
|
||||
const spaces = olMatch[1].length;
|
||||
const subLevel = Math.max(0, Math.floor((spaces - 2) / 2));
|
||||
const indent = subLevel * 12;
|
||||
const style = indent ? ` style="margin-left: ${indent}px"` : '';
|
||||
const itemContent = formatInlineMarkdown(olMatch[3]);
|
||||
result.push(`<li${style}>${itemContent}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 7. Regular paragraph / empty line
|
||||
closeList();
|
||||
if (trimmed === '') {
|
||||
result.push('<div class="md-para-gap"></div>');
|
||||
} else {
|
||||
const lineContent = formatInlineMarkdown(rawLine);
|
||||
result.push(`<div>${lineContent}</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
const codeContent = codeBlockLines.join('\n');
|
||||
result.push(`<pre class="code-block"><code class="lang-${codeBlockLang}">${codeContent}</code></pre>`);
|
||||
}
|
||||
|
||||
closeList();
|
||||
return result.join('\n');
|
||||
}
|
||||
Reference in new issue
Block a user