This commit is contained in:
Gilles Soulier
2026-01-05 13:13:08 +01:00
parent 8e14adafc6
commit 1d177e96a6
149 changed files with 29541 additions and 1 deletions

38
agent/agent-ui/README.md Normal file
View File

@@ -0,0 +1,38 @@
<!--
Created by: Codex
Date: 2026-01-05
Purpose: Desktop UI for Mesh Agent (Tauri)
Refs: CLAUDE.md
-->
# Mesh Agent UI (Tauri)
This is a lightweight desktop UI for the Mesh agent.
## Features (MVP)
- Show agent status (running/stopped)
- Edit and save agent configuration
- Start/stop the agent from the UI
## Dev setup
```bash
cd agent/agent-ui
npm install
```
```bash
cd agent/agent-ui
npm run dev
```
In another terminal, run the Tauri backend:
```bash
cd agent/agent-ui
cargo tauri dev
```
## Notes
- The UI uses the same config file as the CLI agent.
- The agent core runs inside the UI process for now.

91
agent/agent-ui/index.html Normal file
View File

@@ -0,0 +1,91 @@
<!--
Created by: Codex
Date: 2026-01-05
Purpose: Mesh Agent UI shell
Refs: CLAUDE.md
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mesh Agent</title>
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="app">
<header class="topbar">
<div>
<p class="eyebrow">Mesh</p>
<h1>Agent Control</h1>
<p class="subtitle">Desktop UI for the P2P data plane</p>
</div>
<div class="status" id="status-badge">Stopped</div>
</header>
<section class="grid">
<div class="card">
<h2>Status</h2>
<p class="label">State</p>
<p class="value" id="status-text">Stopped</p>
<p class="label">Last error</p>
<p class="value muted" id="error-text">None</p>
<div class="actions">
<button id="start-btn">Start Agent</button>
<button class="ghost" id="stop-btn">Stop Agent</button>
</div>
</div>
<div class="card">
<h2>Config</h2>
<form id="config-form">
<label>
Device ID
<input name="device_id" type="text" />
</label>
<label>
Server URL
<input name="server_url" type="text" />
</label>
<label>
WS URL
<input name="ws_url" type="text" />
</label>
<label>
Auth Token
<input name="auth_token" type="password" />
</label>
<label>
QUIC Port
<input name="quic_port" type="number" min="0" />
</label>
<label>
Log Level
<input name="log_level" type="text" />
</label>
<label>
Gotify URL
<input name="gotify_url" type="text" />
</label>
<label>
Gotify Token
<input name="gotify_token" type="password" />
</label>
<div class="actions">
<button type="button" class="ghost" id="reload-btn">Reload</button>
<button type="submit" id="save-btn">Save</button>
</div>
</form>
</div>
</section>
<footer class="footer">
<p>Agent runs inside this UI process. Close the app to stop it.</p>
</footer>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1020
agent/agent-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"_created_by": "Codex",
"_created_date": "2026-01-05",
"_purpose": "Desktop UI for Mesh Agent (Tauri)",
"_refs": "CLAUDE.md",
"name": "mesh-agent-ui",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tauri-apps/api": "^1.5.0"
},
"devDependencies": {
"typescript": "^5.4.5",
"vite": "^5.4.2"
}
}

View File

@@ -0,0 +1,21 @@
# Created by: Codex
# Date: 2026-01-05
# Purpose: Tauri backend for Mesh Agent UI
# Refs: CLAUDE.md
[package]
name = "mesh-agent-ui"
version = "0.1.0"
description = "Desktop UI for Mesh Agent"
authors = ["Mesh Team"]
edition = "2021"
[dependencies]
mesh_agent = { path = "../..", package = "mesh-agent" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "2.0", features = [] }
tokio = { version = "1.35", features = ["rt-multi-thread", "macros"] }
[build-dependencies]
tauri-build = { version = "2.0", features = [] }

View File

@@ -0,0 +1,8 @@
// Created by: Codex
// Date: 2026-01-05
// Purpose: Build script for Tauri
// Refs: CLAUDE.md
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,94 @@
// Created by: Codex
// Date: 2026-01-05
// Purpose: Tauri commands for Mesh Agent UI
// Refs: CLAUDE.md
use serde::Serialize;
use tokio::sync::Mutex;
use mesh_agent::config::Config;
use mesh_agent::runner::AgentHandle;
#[derive(Default)]
pub struct AgentState {
pub running: bool,
pub last_error: Option<String>,
pub handle: Option<AgentHandle>,
}
pub struct AppState {
pub inner: Mutex<AgentState>,
}
#[derive(Serialize)]
pub struct AgentStatus {
pub running: bool,
pub last_error: Option<String>,
}
impl AgentStatus {
fn from_state(state: &AgentState) -> Self {
Self {
running: state.running,
last_error: state.last_error.clone(),
}
}
}
#[tauri::command]
pub async fn get_status(state: tauri::State<'_, AppState>) -> Result<AgentStatus, String> {
let guard = state.inner.lock().await;
Ok(AgentStatus::from_state(&guard))
}
#[tauri::command]
pub async fn get_config() -> Result<Config, String> {
Config::load().map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn save_config(config: Config) -> Result<(), String> {
config.save_default_path().map_err(|err| err.to_string())
}
#[tauri::command]
pub async fn start_agent(state: tauri::State<'_, AppState>) -> Result<AgentStatus, String> {
{
let guard = state.inner.lock().await;
if guard.running {
return Ok(AgentStatus::from_state(&guard));
}
}
let config = Config::load().map_err(|err| err.to_string())?;
let handle = mesh_agent::runner::start_agent(config)
.await
.map_err(|err| err.to_string())?;
let mut guard = state.inner.lock().await;
guard.handle = Some(handle);
guard.running = true;
guard.last_error = None;
Ok(AgentStatus::from_state(&guard))
}
#[tauri::command]
pub async fn stop_agent(state: tauri::State<'_, AppState>) -> Result<AgentStatus, String> {
let handle = {
let mut guard = state.inner.lock().await;
guard.running = false;
guard.last_error = None;
guard.handle.take()
};
if let Some(handle) = handle {
if let Err(err) = handle.stop().await {
let mut guard = state.inner.lock().await;
guard.last_error = Some(err.to_string());
}
}
let guard = state.inner.lock().await;
Ok(AgentStatus::from_state(&guard))
}

View File

@@ -0,0 +1,30 @@
// Created by: Codex
// Date: 2026-01-05
// Purpose: Tauri entrypoint for Mesh Agent UI
// Refs: CLAUDE.md
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod commands;
use commands::{AppState, AgentState};
use tokio::sync::Mutex;
fn main() {
let result = tauri::Builder::default()
.manage(AppState {
inner: Mutex::new(AgentState::default()),
})
.invoke_handler(tauri::generate_handler![
commands::get_status,
commands::get_config,
commands::save_config,
commands::start_agent,
commands::stop_agent,
])
.run(tauri::generate_context!());
if let Err(err) = result {
eprintln!("Mesh Agent UI failed to start: {}", err);
}
}

View File

@@ -0,0 +1,25 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Mesh Agent",
"version": "0.1.0",
"identifier": "com.mesh.agent",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Mesh Agent",
"width": 1080,
"height": 720,
"resizable": true
}
],
"security": {
"csp": null
}
}
}

117
agent/agent-ui/src/main.ts Normal file
View File

@@ -0,0 +1,117 @@
// Created by: Codex
// Date: 2026-01-05
// Purpose: UI logic for Mesh Agent desktop app
// Refs: CLAUDE.md
import { invoke } from "@tauri-apps/api/tauri";
type Config = {
device_id: string;
server_url: string;
ws_url: string;
auth_token: string | null;
gotify_url: string | null;
gotify_token: string | null;
quic_port: number;
log_level: string;
};
type AgentStatus = {
running: boolean;
last_error: string | null;
};
const statusBadge = document.querySelector<HTMLDivElement>("#status-badge");
const statusText = document.querySelector<HTMLParagraphElement>("#status-text");
const errorText = document.querySelector<HTMLParagraphElement>("#error-text");
const form = document.querySelector<HTMLFormElement>("#config-form");
const startBtn = document.querySelector<HTMLButtonElement>("#start-btn");
const stopBtn = document.querySelector<HTMLButtonElement>("#stop-btn");
const reloadBtn = document.querySelector<HTMLButtonElement>("#reload-btn");
if (!statusBadge || !statusText || !errorText || !form || !startBtn || !stopBtn || !reloadBtn) {
throw new Error("UI elements missing");
}
const toOptional = (value: FormDataEntryValue | null): string | null => {
if (!value) return null;
const trimmed = value.toString().trim();
return trimmed.length ? trimmed : null;
};
const getFormData = (): Config => {
const data = new FormData(form);
return {
device_id: String(data.get("device_id") || ""),
server_url: String(data.get("server_url") || ""),
ws_url: String(data.get("ws_url") || ""),
auth_token: toOptional(data.get("auth_token")),
gotify_url: toOptional(data.get("gotify_url")),
gotify_token: toOptional(data.get("gotify_token")),
quic_port: Number(data.get("quic_port") || 0),
log_level: String(data.get("log_level") || "info")
};
};
const setFormData = (config: Config) => {
(form.elements.namedItem("device_id") as HTMLInputElement).value = config.device_id;
(form.elements.namedItem("server_url") as HTMLInputElement).value = config.server_url;
(form.elements.namedItem("ws_url") as HTMLInputElement).value = config.ws_url;
(form.elements.namedItem("auth_token") as HTMLInputElement).value = config.auth_token || "";
(form.elements.namedItem("gotify_url") as HTMLInputElement).value = config.gotify_url || "";
(form.elements.namedItem("gotify_token") as HTMLInputElement).value = config.gotify_token || "";
(form.elements.namedItem("quic_port") as HTMLInputElement).value = String(config.quic_port);
(form.elements.namedItem("log_level") as HTMLInputElement).value = config.log_level;
};
const setStatus = (status: AgentStatus) => {
const text = status.running ? "Running" : "Stopped";
statusText.textContent = text;
statusBadge.textContent = text;
statusBadge.classList.toggle("stopped", !status.running);
errorText.textContent = status.last_error || "None";
};
const loadConfig = async () => {
const config = await invoke<Config>("get_config");
setFormData(config);
};
const saveConfig = async () => {
const config = getFormData();
await invoke("save_config", { config });
};
const startAgent = async () => {
const status = await invoke<AgentStatus>("start_agent");
setStatus(status);
};
const stopAgent = async () => {
const status = await invoke<AgentStatus>("stop_agent");
setStatus(status);
};
form.addEventListener("submit", async (event) => {
event.preventDefault();
await saveConfig();
});
reloadBtn.addEventListener("click", async () => {
await loadConfig();
});
startBtn.addEventListener("click", async () => {
await startAgent();
});
stopBtn.addEventListener("click", async () => {
await stopAgent();
});
loadConfig()
.then(() => invoke<AgentStatus>("get_status"))
.then(setStatus)
.catch((err) => {
errorText.textContent = String(err);
});

View File

@@ -0,0 +1,195 @@
/* Created by: Codex */
/* Date: 2026-01-05 */
/* Purpose: UI styling for Mesh Agent desktop app */
/* Refs: CLAUDE.md */
:root {
--bg: #0f1113;
--panel: #181c1f;
--panel-alt: #101315;
--ink: #f2f1ec;
--muted: #b6b1a7;
--accent: #ff9e3d;
--accent-2: #53d0b3;
--danger: #f05365;
--border: rgba(242, 241, 236, 0.1);
--shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
--radius: 18px;
--font-sans: "Space Grotesk", "IBM Plex Sans", "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font-sans);
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(255, 158, 61, 0.18), transparent 45%),
radial-gradient(circle at 20% 40%, rgba(83, 208, 179, 0.15), transparent 50%),
linear-gradient(160deg, #0e0f10, #15191c 50%, #0f1214);
min-height: 100vh;
}
#app {
max-width: 1100px;
margin: 0 auto;
padding: 48px 28px 40px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
margin-bottom: 32px;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.3em;
font-size: 12px;
color: var(--muted);
margin: 0 0 6px;
}
h1 {
margin: 0 0 6px;
font-size: 36px;
font-weight: 600;
}
.subtitle {
margin: 0;
color: var(--muted);
}
.status {
padding: 10px 18px;
border-radius: 999px;
background: rgba(83, 208, 179, 0.2);
color: var(--accent-2);
border: 1px solid rgba(83, 208, 179, 0.4);
font-weight: 600;
}
.status.stopped {
background: rgba(240, 83, 101, 0.15);
color: var(--danger);
border-color: rgba(240, 83, 101, 0.35);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 22px;
}
.card {
background: linear-gradient(160deg, rgba(24, 28, 31, 0.9), rgba(16, 19, 21, 0.92));
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
box-shadow: var(--shadow);
}
h2 {
margin: 0 0 18px;
font-size: 20px;
}
.label {
margin: 12px 0 4px;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.value {
margin: 0;
font-size: 16px;
}
.value.muted {
color: var(--muted);
}
form {
display: grid;
gap: 12px;
}
label {
display: grid;
gap: 6px;
font-size: 13px;
color: var(--muted);
}
input {
background: var(--panel-alt);
border: 1px solid var(--border);
color: var(--ink);
padding: 10px 12px;
border-radius: 12px;
font-size: 14px;
}
input:focus {
outline: 1px solid var(--accent);
border-color: transparent;
}
.actions {
display: flex;
gap: 12px;
margin-top: 12px;
}
button {
border: none;
border-radius: 12px;
padding: 10px 16px;
font-weight: 600;
color: #1b1b1b;
background: var(--accent);
cursor: pointer;
transition: transform 120ms ease, box-shadow 120ms ease;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(255, 158, 61, 0.22);
}
button.ghost {
background: transparent;
color: var(--ink);
border: 1px solid var(--border);
box-shadow: none;
}
button.ghost:hover {
box-shadow: none;
border-color: rgba(242, 241, 236, 0.25);
}
.footer {
margin-top: 26px;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 720px) {
#app {
padding: 32px 18px 28px;
}
.topbar {
flex-direction: column;
align-items: flex-start;
}
}

View File

@@ -0,0 +1,18 @@
{
"_created_by": "Codex",
"_created_date": "2026-01-05",
"_purpose": "TypeScript config for Mesh Agent UI",
"_refs": "CLAUDE.md",
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,14 @@
// Created by: Codex
// Date: 2026-01-05
// Purpose: Vite config for Mesh Agent UI
// Refs: CLAUDE.md
import { defineConfig } from "vite";
export default defineConfig({
base: "./",
server: {
port: 5173,
strictPort: true
}
});