Monorepo for Aesthetic.Computer aesthetic.computer

Remote Device Control for Artery TUI#

Overview#

Enable artery-tui to list, address, and control externally connected local development devices (phones, tablets, other browsers) via the session server. This allows developers to "jump" specific devices to pieces, sync state, and debug across multiple clients.

Implementation Status#

Feature Status Notes
Device list API (/devices) ✅ Done Returns connected clients with metadata
Targeted jump (/jump/:target) ✅ Done By ID, IP, or handle
Targeted reload (/reload/:target) ✅ Done Same targeting as jump
Device naming (/device/:id/name) ✅ Done PUT to set device name
dev:identity message ✅ Done Sent on WebSocket connect with letter (A-Z)
devIdentity in disk.mjs ✅ Done Tracks host name, connection info, device letter
LAN mode badge (HUD) ✅ Done Pixel-art letter (A-Z) in top-right corner
LAN mode on prompt curtain ✅ Done Shows host name on starfield
Device letters (A-Z) ✅ Done Assigned by session server based on connection order
Artery TUI devices mode ✅ Done 'V' key for device list view
Artery TUI QR code ✅ Done Scannable QR in menu (uses HOST_IP env var)
Remote log forwarding ✅ Done dev:log from connected devices to session server
Products carousel ⏸️ Disabled Disabled for faster dev loading

Current Architecture#

Full Message Flow (Session WebSocket → disk.mjs)#

┌─────────────────┐    POST /jump     ┌─────────────────────┐
│  artery-tui     │ ───────────────►  │  session-server     │
│  (Node.js CLI)  │                   │  (session.mjs)      │
└─────────────────┘                   └──────────┬──────────┘
                                                 │
                                     everyone(pack("jump", {piece}))
                                                 │ WebSocket broadcast
                                                 ▼
┌─────────────────┐                   ┌─────────────────────┐
│  disk.mjs       │ ◄──── receive ────│  socket.mjs         │
│  (AC runtime)   │                   │  (WebSocket client) │
└────────┬────────┘                   └─────────────────────┘
         │
         │ $commonApi.jump(content.piece)
         ▼
┌─────────────────┐
│  Browser nav    │
│  (piece loads)  │
└─────────────────┘

1. Session Server (session-server/session.mjs)#

Tracks connected devices with rich metadata:

// Unified client tracking (line ~172)
const clients = {}; // Map of connection ID to:
// { handle, user, location, ip, geo, websocket: true/false, udp: true/false }

Each WebSocket connection includes:

  • Connection ID: Numeric identifier (0, 1, 2, ...)
  • IP Address: e.g., 192.168.1.100 (phone), 172.17.0.1 (local)
  • User Agent: Identifies device type (Android/iOS/Desktop)
  • Handle: Username if logged in
  • Geolocation: Country, region, city (if available)
  • Current location (piece): Where the client is navigated (via location:broadcast)

2. Socket Client (system/.../lib/socket.mjs)#

Browser-side WebSocket class that connects to session server:

// socket.mjs - Client-side WebSocket wrapper
export class Socket {
  id;              // Connection ID assigned by server
  connected = false;
  
  connect(host, receive, reload, protocol, connectCallback, disconnectCallback) {
    this.#ws = new WebSocket(`${protocol}://${host}`);
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      this.#preReceive(msg, receive, reload, sendToBIOS);
    };
  }
  
  send(type, content) {
    this.#ws.send(JSON.stringify({ type, content }));
  }
}

3. disk.mjs Message Handling (system/.../lib/disk.mjs)#

Connects socket and handles incoming messages including jump:

// disk.mjs line ~6890 - Socket connection with message receiver
socket?.connect(
  url.host + url.pathname,
  (id, type, content) => {
    // 🎯 Jump to a specific piece!
    if (type === "jump") {
      $commonApi.jump(content.piece);
    }
    receiver?.(id, type, content); // Pass to piece
  },
  $commonApi.reload,  // For live reload
  "wss",
  () => { /* connected callback */ },
  () => { /* disconnected callback */ }
);

// Also handles in main receive function (~line 8493)
if (type === "jump") {
  $commonApi.jump(content.piece, ahistorical, alias);
  return;
}

// And piece-reload (~line 8444)
if (type === "piece-reload") {
  $commonApi.reload({ source: content.source, ... });
  return;
}

Current Jump Implementation (Broadcasts to ALL)#

The /jump HTTP endpoint broadcasts to ALL connected clients:

// Line 295-310 in session.mjs
fastify.post("/jump", async (req) => {
  const { piece } = req.body;
  
  // Broadcast to all browser clients (no targeting!)
  everyone(pack("jump", { piece }, "pieces"));
  
  return { msg: "Jump request sent!", piece };
});

Session Pane Output (from Emacs 📋-session buffer)#

Shows connected devices like:

🔌 WebSocket connection received: {
  "host": "192.168.1.88:8889",
  "origin": "https://192.168.1.88:8888",
  "userAgent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36...",
  "remoteAddress": "192.168.1.100"
}
🧏 Someone joined: 3:192.168.1.100 Online: 2  🫂

Existing Dev Message Types#

The session server already supports these dev-time message types:

Type Direction Purpose
reload Server→Client Hot reload piece code
jump Server→Client Navigate to piece
code Server→Client Live code update (codeChannel)
piece-reload Server→Client Reload with new KidLisp source
location:broadcast Client→Server Report current piece
code-channel:sub Client→Server Subscribe to code updates

Proposed Features#

Phase 1: Device List & Status#

Add a "Devices" mode to artery-tui that shows connected clients.

TUI Menu Addition:

{ key: 'D', label: 'Devices', desc: 'List connected clients', action: () => this.enterDevicesMode() }

Display Format:

╔══════════════════ CONNECTED DEVICES ═══════════════════╗
║  # │ IP              │ Type    │ Piece    │ Handle    ║
╠═══════════════════════════════════════════════════════╣
║  0 │ 172.17.0.1      │ VS Code │ prompt   │ -         ║
║  1 │ 192.168.1.100   │ Android │ prompt   │ @jeffrey  ║
║  2 │ 192.168.1.205   │ iPhone  │ bleep    │ -         ║
╚═══════════════════════════════════════════════════════╝
 [j] Jump selected  [J] Jump all  [r] Refresh  [q] Back

API Endpoint (new):

// GET /devices - Returns connected client list
fastify.get("/devices", async (req) => {
  return getClientStatus(); // Already exists!
});

Phase 2: Targeted Jump#

Add ability to jump specific devices by connection ID or IP.

New API Endpoint:

// POST /jump/:target - Jump specific device(s)
fastify.post("/jump/:target", async (req) => {
  const { target } = req.params;  // Connection ID, IP, or handle
  const { piece, ahistorical, alias } = req.body;
  
  // Find connection(s) matching target
  const matches = targetClients(target);
  
  if (matches.length === 0) {
    return { error: "No matching device", target };
  }
  
  // Send jump only to matching connections
  matches.forEach(ws => {
    ws.send(pack("jump", { piece, ahistorical, alias }, "pieces"));
  });
  
  return { 
    msg: "Targeted jump sent", 
    piece, 
    targets: matches.length 
  };
});

// Helper: Find connections by ID, IP, or handle
function targetClients(target) {
  const results = [];
  for (const [id, ws] of Object.entries(connections)) {
    const client = clients[id];
    if (
      String(id) === String(target) ||
      client?.ip === target ||
      client?.ip?.replace('::ffff:', '') === target ||
      client?.handle === `@${target}` ||
      client?.handle === target
    ) {
      if (ws?.readyState === WebSocket.OPEN) {
        results.push(ws);
      }
    }
  }
  return results;
}

Also add targeted reload:

// POST /reload/:target - Reload specific device(s)
fastify.post("/reload/:target", async (req) => {
  const { target } = req.params;
  const matches = targetClients(target);
  
  matches.forEach(ws => {
    ws.send(pack("reload", req.body, "pieces"));
  });
  
  return { msg: "Targeted reload sent", targets: matches.length };
});

TUI Usage:

  • Select device with arrow keys
  • Press j to jump selected device
  • Press J to jump all devices
  • Type piece name or use picker

Phase 3: Device Naming & Groups#

Allow assigning friendly names to devices for easier management.

Device Registry (Redis or file-based):

// Store in Redis or local file
deviceNames = {
  "192.168.1.100": { name: "Jeffrey's Pixel", group: "phones" },
  "192.168.1.205": { name: "Test iPhone", group: "phones" }
}

TUI Display with names:

║  1 │ Jeffrey's Pixel │ Android │ prompt   │ @jeffrey  ║

Group Commands:

  • jump phones prompt — Jump all devices in "phones" group
  • jump all prompt — Jump every connected device

Phase 4: Sync & Debug Features#

Location Sync:

  • Option to auto-sync piece navigation across all devices
  • Useful for demos and synchronized testing

Device-Specific Console:

  • Stream console logs from specific device to TUI
  • Filter logs by connection ID

Screenshot/Record:

  • Trigger screenshot on specific device
  • Record session from device for debugging

Implementation Plan#

1. Session Server Changes#

File: session-server/session.mjs

// Add after line 310 (after existing /jump endpoint)

// GET /devices - List all connected clients with metadata
fastify.get("/devices", async () => {
  return {
    devices: getClientStatus(),
    timestamp: Date.now()
  };
});

// POST /jump/:target - Targeted jump (by ID, IP, or handle)
if (dev) {
  fastify.post("/jump/:target", async (req) => {
    const { target } = req.params;
    const { piece, ahistorical, alias } = req.body;
    
    const targeted = targetClients(target);
    if (targeted.length === 0) {
      return { error: "No matching device", target };
    }
    
    targeted.forEach(ws => {
      ws.send(pack("jump", { piece, ahistorical, alias }, "pieces"));
    });
    
    return { msg: "Targeted jump sent", piece, count: targeted.length };
  });

  // POST /reload/:target - Targeted reload
  fastify.post("/reload/:target", async (req) => {
    const { target } = req.params;
    const targeted = targetClients(target);
    
    targeted.forEach(ws => {
      ws.send(pack("reload", req.body, "pieces"));
    });
    
    return { msg: "Targeted reload sent", count: targeted.length };
  });
  
  // POST /piece-reload/:target - Targeted KidLisp reload
  fastify.post("/piece-reload/:target", async (req) => {
    const { target } = req.params;
    const { source, createCode, authToken } = req.body;
    const targeted = targetClients(target);
    
    targeted.forEach(ws => {
      ws.send(pack("piece-reload", { source, createCode, authToken }, "kidlisp"));
    });
    
    return { msg: "Targeted piece-reload sent", count: targeted.length };
  });
}

// Helper: Find connections by ID, IP, or handle
function targetClients(target) {
  if (target === 'all') {
    return Object.values(connections).filter(ws => ws?.readyState === WebSocket.OPEN);
  }
  
  const results = [];
  for (const [id, ws] of Object.entries(connections)) {
    const client = clients[id];
    const cleanTarget = target.replace('@', '');
    const cleanIp = client?.ip?.replace('::ffff:', '');
    
    if (
      String(id) === String(target) ||
      cleanIp === target ||
      client?.handle === `@${cleanTarget}` ||
      client?.handle === cleanTarget
    ) {
      if (ws?.readyState === WebSocket.OPEN) {
        results.push(ws);
      }
    }
  }
  return results;
}

2. Artery TUI Changes#

File: artery/artery-tui.mjs

Add after line ~465 in menuItems:

{ key: 'D', label: 'Devices', desc: 'List & control connected clients', action: () => this.enterDevicesMode() },

New mode implementation:

async enterDevicesMode() {
  this.mode = 'devices';
  this.devicesLoading = true;
  this.devices = [];
  this.deviceIndex = 0;
  await this.refreshDevices();
  this.devicesLoading = false;
  this.render();
}

async refreshDevices() {
  try {
    const https = await import('https');
    const agent = new https.Agent({ rejectUnauthorized: false });
    const res = await fetch('https://localhost:8889/devices', { agent });
    const data = await res.json();
    this.devices = data.devices || [];
  } catch (e) {
    this.devices = [];
    this.setStatus(`Failed to fetch devices: ${e.message}`);
  }
}

async jumpDevice(target, piece) {
  try {
    const https = await import('https');
    const agent = new https.Agent({ rejectUnauthorized: false });
    const res = await fetch(`https://localhost:8889/jump/${target}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ piece }),
      agent
    });
    const data = await res.json();
    if (data.error) {
      this.setStatus(`Jump failed: ${data.error}`);
    } else {
      this.setStatus(`Jumped ${data.count} device(s) to ${piece}`);
    }
  } catch (e) {
    this.setStatus(`Jump failed: ${e.message}`);
  }
}

async reloadDevice(target, options = {}) {
  try {
    const https = await import('https');
    const agent = new https.Agent({ rejectUnauthorized: false });
    const res = await fetch(`https://localhost:8889/reload/${target}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(options),
      agent
    });
    const data = await res.json();
    this.setStatus(`Reloaded ${data.count} device(s)`);
  } catch (e) {
    this.setStatus(`Reload failed: ${e.message}`);
  }
}

renderDevicesMode() {
  // Render device list table with selection highlight
  // Columns: #, IP, Type, Piece, Handle
  // Show key bindings: [j]ump [r]eload [R]efresh [q]uit
}

3. Artery Client Helper (Optional - for CLI usage)#

File: artery/artery.mjs

Add convenience methods for device control via HTTP (not CDP):

// Device control via session server HTTP API
async getDevices() {
  const https = await import('https');
  const agent = new https.Agent({ rejectUnauthorized: false });
  const res = await fetch('https://localhost:8889/devices', { agent });
  return await res.json();
}

async jumpDevice(target, piece, options = {}) {
  const https = await import('https');
  const agent = new https.Agent({ rejectUnauthorized: false });
  const res = await fetch(`https://localhost:8889/jump/${target}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ piece, ...options }),
    agent
  });
  return await res.json();
}

async jumpAll(piece, options = {}) {
  return this.jumpDevice('all', piece, options);
}

async reloadDevice(target, options = {}) {
  const https = await import('https');
  const agent = new https.Agent({ rejectUnauthorized: false });
  const res = await fetch(`https://localhost:8889/reload/${target}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(options),
    agent
  });
  return await res.json();
}

Device Detection Heuristics#

Parse User-Agent for device type:

function getDeviceType(userAgent) {
  if (!userAgent) return 'Unknown';
  if (userAgent.includes('Android')) return 'Android';
  if (userAgent.includes('iPhone') || userAgent.includes('iPad')) return 'iOS';
  if (userAgent.includes('Code/')) return 'VS Code';
  if (userAgent.includes('Chrome')) return 'Chrome';
  if (userAgent.includes('Firefox')) return 'Firefox';
  if (userAgent.includes('Safari')) return 'Safari';
  return 'Browser';
}

CLI Quick Commands (Future)#

For fast iteration without TUI:

# List devices
artery devices

# Jump specific device
artery jump 192.168.1.100 prompt
artery jump @jeffrey bleep

# Jump all
artery jump --all line

Priority Order#

  1. Device List APIGET /devices endpoint ✓ (already exists via getClientStatus)
  2. Targeted Jump APIPOST /jump/:target endpoint
  3. Targeted Reload APIPOST /reload/:target endpoint
  4. TUI Devices Mode — List display with selection
  5. TUI Jump/Reload from Devices — Select device, enter piece, send command
  6. Device Type Detection — Parse User-Agent for icons
  7. Device Naming — Store custom names (Redis or file)
  8. CLI Commands — Quick command-line access

Testing Strategy#

  1. Unit test targetClients() helper function
  2. Integration test /devices API returns correct data
  3. Integration test /jump/:target sends to correct WebSocket
  4. Manual test with phone connected over local network:
    • Connect phone to https://192.168.x.x:8888
    • See phone appear in device list
    • Jump phone to specific piece
    • Verify phone navigates correctly
    • Test reload sends to correct device

Message Type Reference#

Message Type Handler Location Description
jump disk.mjs:8493, disk.mjs:6895 Navigate to piece
reload socket.mjs (preReceive) Hot reload piece code
piece-reload disk.mjs:8444 Reload with KidLisp source
code socket.mjs (preReceive) Live code channel update
connected socket.mjs (preReceive) Connection established
location:broadcast session.mjs Client reports current piece

Notes#

  • The session server already tracks clients with IPs and metadata
  • The /status endpoint provides full client info (used by dashboard)
  • CDP-based jump (in artery.mjs) only works for VS Code Simple Browser
  • WebSocket-based jump (proposed) works for ANY connected client
  • Phone connections show up with LAN IPs like 192.168.1.xxx
  • All dev endpoints should be gated with if (dev) check
  • disk.mjs receives messages via socket.mjs's receive callback