Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

machines: outside-in remote prompt with call-and-response

Generalize the existing /machines command channel from "jump piece-name"
into a free-text prompt that runs through the device's prompt.mjs execute()
exactly as if you were typing locally. Anything the local prompt accepts
(piece names, kidlisp expressions, link/wifi/theme builtins, $code aliases)
now works from the web dashboard.

- session-server: forward msg.args.text alongside target on cmd:"prompt".
- machines.c/.h: widen cmd_target to 2048 for prompt payloads, add an
escape-aware json extractor so quoted strings survive transit, and add
machines_send_response() that ships {type:"command-response", data:{ok,output}}.
- ac-native: new ACRuntime fields (pending_prompt_text/_id) staged when a
prompt cmd arrives, then route the runtime through the prompt piece.
- js-bindings: system.consumePromptCmd() and system.machinesResponse(id, output, ok).
- native prompt.mjs: boot consumes the staged cmd, calls execute(), captures
whatever message landed, and ships it back through machinesResponse().
- web machines.mjs: rebrand [Jump] → [Prompt] free-text input, send cmd:"prompt"
with {text}, mirror submitted text + device reply as colored log entries.

+206 -17
+21
fedac/native/pieces/prompt.mjs
··· 197 197 const raw = system?.readFile?.(CREDS_PATH); 198 198 if (raw) savedCreds = JSON.parse(raw); 199 199 } catch (_) {} 200 + 201 + // Outside-in remote prompt: ac-native stages text + commandId here when a 202 + // viewer publishes cmd:"prompt" through /machines. Run it through the same 203 + // execute() that local typing uses, then ship whatever message landed back. 204 + const remote = system?.consumePromptCmd?.(); 205 + if (remote && remote.text) { 206 + const before = message; 207 + message = ""; 208 + let ok = 1; 209 + try { 210 + execute(remote.text, system); 211 + } catch (err) { 212 + ok = 0; 213 + message = "error: " + (err?.message || String(err)); 214 + } 215 + const reply = message || "(no output)"; 216 + try { system?.machinesResponse?.(remote.id, reply, ok); } catch (_) {} 217 + // Restore prior message so the local screen doesn't get hijacked when 218 + // execute() didn't produce visible feedback. 219 + if (!message) message = before; 220 + } 200 221 } 201 222 202 223 function act({ event: e, system, sound }) {
+14 -1
fedac/native/src/ac-native.c
··· 456 456 // Generated on first boot, read back on subsequent boots. 457 457 // Accessible from js-bindings.c via extern. 458 458 char g_machine_id[64] = {0}; 459 - static ACMachines g_machines = {0}; 459 + ACMachines g_machines = {0}; 460 460 461 461 static void init_machine_id(void) { 462 462 FILE *f = fopen("/mnt/.machine-id", "r"); ··· 4571 4571 strncpy(rt->jump_target, g_machines.cmd_target, sizeof(rt->jump_target) - 1); 4572 4572 rt->jump_requested = 1; 4573 4573 ac_log("[machines] jump → %s\n", g_machines.cmd_target); 4574 + } else if (strcmp(g_machines.cmd_type, "prompt") == 0 && g_machines.cmd_target[0]) { 4575 + // Stage text + id for prompt.mjs to consume on boot, 4576 + // then route the runtime through the prompt piece. 4577 + strncpy(rt->pending_prompt_text, g_machines.cmd_target, 4578 + sizeof(rt->pending_prompt_text) - 1); 4579 + rt->pending_prompt_text[sizeof(rt->pending_prompt_text) - 1] = 0; 4580 + strncpy(rt->pending_prompt_id, g_machines.cmd_id, 4581 + sizeof(rt->pending_prompt_id) - 1); 4582 + rt->pending_prompt_id[sizeof(rt->pending_prompt_id) - 1] = 0; 4583 + rt->pending_prompt_cmd = 1; 4584 + strncpy(rt->jump_target, "prompt", sizeof(rt->jump_target) - 1); 4585 + rt->jump_requested = 1; 4586 + ac_log("[machines] prompt → %.80s\n", g_machines.cmd_target); 4574 4587 } else if (strcmp(g_machines.cmd_type, "update") == 0) { 4575 4588 // Jump to notepat which handles OTA updates 4576 4589 strncpy(rt->jump_target, "notepat", sizeof(rt->jump_target) - 1);
+42
fedac/native/src/js-bindings.c
··· 20 20 #include <errno.h> 21 21 #include "qrcodegen.h" 22 22 #include <alsa/asoundlib.h> 23 + #include "machines.h" 23 24 #ifdef HAVE_RAYLIB 24 25 #include "raylib-soft.h" 25 26 #endif ··· 2629 2630 2630 2631 graph_paste(current_rt->graph, src, dx, dy); 2631 2632 return JS_UNDEFINED; 2633 + } 2634 + 2635 + // system.consumePromptCmd() — pop a pending outside-in prompt command staged 2636 + // by ac-native.c (machines daemon → cmd:"prompt"). Returns {id, text} once 2637 + // per arrival and clears the slot, or null if nothing's queued. 2638 + static JSValue js_consume_prompt_cmd(JSContext *ctx, JSValueConst this_val, 2639 + int argc, JSValueConst *argv) { 2640 + (void)this_val; (void)argc; (void)argv; 2641 + if (!current_rt || !current_rt->pending_prompt_cmd) return JS_NULL; 2642 + JSValue obj = JS_NewObject(ctx); 2643 + JS_SetPropertyStr(ctx, obj, "id", JS_NewString(ctx, current_rt->pending_prompt_id)); 2644 + JS_SetPropertyStr(ctx, obj, "text", JS_NewString(ctx, current_rt->pending_prompt_text)); 2645 + current_rt->pending_prompt_cmd = 0; 2646 + current_rt->pending_prompt_text[0] = 0; 2647 + current_rt->pending_prompt_id[0] = 0; 2648 + return obj; 2649 + } 2650 + 2651 + // system.machinesResponse(id, output, ok?) — ship a command-response back 2652 + // through the existing /machines WebSocket session so the viewer's UI can 2653 + // render what the device produced for an outside-in prompt. 2654 + static JSValue js_machines_response(JSContext *ctx, JSValueConst this_val, 2655 + int argc, JSValueConst *argv) { 2656 + (void)this_val; 2657 + if (argc < 2) return JS_FALSE; 2658 + const char *id = JS_ToCString(ctx, argv[0]); 2659 + const char *output = JS_ToCString(ctx, argv[1]); 2660 + int ok = 1; 2661 + if (argc >= 3) ok = JS_ToBool(ctx, argv[2]); 2662 + if (id && output) { 2663 + machines_send_response(&g_machines, id, "prompt", output, ok); 2664 + } 2665 + if (id) JS_FreeCString(ctx, id); 2666 + if (output) JS_FreeCString(ctx, output); 2667 + return JS_TRUE; 2632 2668 } 2633 2669 2634 2670 // system.raylibTest(painting, frame?) — fill a painting buffer with a ··· 7104 7140 // compiled in, so pieces can probe availability. 7105 7141 JS_SetPropertyStr(ctx, sys, "raylibTest", 7106 7142 JS_NewCFunction(ctx, js_raylib_test, "raylibTest", 2)); 7143 + 7144 + // Outside-in remote prompt round-trip (cmd:"prompt" via /machines). 7145 + JS_SetPropertyStr(ctx, sys, "consumePromptCmd", 7146 + JS_NewCFunction(ctx, js_consume_prompt_cmd, "consumePromptCmd", 0)); 7147 + JS_SetPropertyStr(ctx, sys, "machinesResponse", 7148 + JS_NewCFunction(ctx, js_machines_response, "machinesResponse", 3)); 7107 7149 7108 7150 // Printer detection and raw printing 7109 7151 JS_SetPropertyStr(ctx, sys, "listPrinters",
+8
fedac/native/src/js-bindings.h
··· 94 94 char jump_params[8][64]; // colon-separated params (e.g. "chat:clock" → ["clock"]) 95 95 int jump_param_count; 96 96 97 + // Outside-in remote prompt (cmd:"prompt" via /machines). 98 + // ac-native.c stages text + commandId here when a remote prompt arrives; 99 + // prompt.mjs's boot consumes it via system.consumePromptCmd() and replies 100 + // through system.machinesResponse(id, output, ok). 101 + volatile int pending_prompt_cmd; 102 + char pending_prompt_text[2048]; 103 + char pending_prompt_id[32]; 104 + 97 105 // PTY terminal emulator 98 106 ACPty pty; 99 107 int pty_active; // 1 = PTY session is running
+63
fedac/native/src/machines.c
··· 76 76 return len; 77 77 } 78 78 79 + // Escape-aware JSON string extraction: respects \", \\, \n, \r, \t so values 80 + // like `(format t "hi")` survive transit. Stops at the first un-escaped quote. 81 + static int json_get_str_unesc(const char *json, const char *key, char *out, int out_sz) { 82 + char needle[128]; 83 + snprintf(needle, sizeof(needle), "\"%s\":\"", key); 84 + const char *p = strstr(json, needle); 85 + if (!p) return -1; 86 + p += strlen(needle); 87 + int j = 0; 88 + while (*p && j < out_sz - 1) { 89 + if (*p == '"') break; 90 + if (*p == '\\' && p[1]) { 91 + char c = p[1]; 92 + if (c == '"') out[j++] = '"'; 93 + else if (c == '\\') out[j++] = '\\'; 94 + else if (c == 'n') out[j++] = '\n'; 95 + else if (c == 'r') out[j++] = '\r'; 96 + else if (c == 't') out[j++] = '\t'; 97 + else if (c == '/') out[j++] = '/'; 98 + else { // unknown escape — keep verbatim 99 + out[j++] = '\\'; 100 + if (j < out_sz - 1) out[j++] = c; 101 + } 102 + p += 2; 103 + } else { 104 + out[j++] = *p++; 105 + } 106 + } 107 + out[j] = 0; 108 + return j; 109 + } 110 + 79 111 // Escape a string for JSON embedding (backslash, quotes, control chars). 80 112 // Returns number of bytes written (excluding null terminator). 81 113 static int json_escape(const char *in, int in_len, char *out, int out_sz) { ··· 340 372 ac_log("[machines] rebooting by remote command\n"); 341 373 sync(); 342 374 reboot(0x01234567); // LINUX_REBOOT_CMD_RESTART 375 + } else if (strcmp(cmd, "prompt") == 0) { 376 + // Free-text remote prompt: feed `text` straight into prompt.mjs's 377 + // execute() on the device. Use the escape-aware extractor so 378 + // strings/parens/newlines in the payload survive. 379 + char text[2048] = ""; 380 + json_get_str_unesc(raw, "text", text, sizeof(text)); 381 + if (text[0] && !m->cmd_pending) { 382 + strncpy(m->cmd_type, "prompt", sizeof(m->cmd_type) - 1); 383 + m->cmd_type[sizeof(m->cmd_type) - 1] = 0; 384 + strncpy(m->cmd_target, text, sizeof(m->cmd_target) - 1); 385 + m->cmd_target[sizeof(m->cmd_target) - 1] = 0; 386 + strncpy(m->cmd_id, cmd_id, sizeof(m->cmd_id) - 1); 387 + m->cmd_id[sizeof(m->cmd_id) - 1] = 0; 388 + m->cmd_pending = 1; 389 + } 343 390 } else if (strcmp(cmd, "jump") == 0 || 344 391 strcmp(cmd, "update") == 0 || 345 392 strcmp(cmd, "request-logs") == 0) { ··· 422 469 if (m->connected && m->ws->connected) { 423 470 sq_drain_one(m); 424 471 } 472 + } 473 + 474 + void machines_send_response(ACMachines *m, const char *cmd_id, 475 + const char *cmd, const char *output, int ok) { 476 + if (!m || !cmd_id || !cmd) return; 477 + char escaped[8192]; 478 + int olen = output ? (int)strlen(output) : 0; 479 + json_escape(output ? output : "", olen, escaped, sizeof(escaped)); 480 + char msg[12288]; 481 + snprintf(msg, sizeof(msg), 482 + "{\"type\":\"command-response\"," 483 + "\"commandId\":\"%s\"," 484 + "\"command\":\"%s\"," 485 + "\"data\":{\"ok\":%s,\"output\":\"%s\"}}", 486 + cmd_id, cmd, ok ? "true" : "false", escaped); 487 + sq_push(m, msg); 425 488 } 426 489 427 490 void machines_flush_logs(ACMachines *m) {
+12 -2
fedac/native/src/machines.h
··· 28 28 29 29 // Pending command from server → forwarded to JS runtime 30 30 volatile int cmd_pending; 31 - char cmd_type[32]; // "jump", "reboot", "update", "request-logs" 32 - char cmd_target[128]; // e.g. piece name for "jump" 31 + char cmd_type[32]; // "jump" | "prompt" | "reboot" | "update" | "request-logs" 32 + char cmd_target[2048]; // piece name for "jump"; free-text for "prompt" 33 33 char cmd_id[32]; // commandId for ack 34 34 } ACMachines; 35 35 36 + // Singleton instance defined in ac-native.c. Exposed so js-bindings.c can 37 + // queue command-response messages out through the same WebSocket session. 38 + extern ACMachines g_machines; 39 + 36 40 // Call once at startup (after init_machine_id, before main loop) 37 41 void machines_init(ACMachines *m); 38 42 ··· 42 46 43 47 // Flush final logs before shutdown (blocking drain of send queue) 44 48 void machines_flush_logs(ACMachines *m); 49 + 50 + // Send a command-response back to the viewer for an outside-in prompt. 51 + // `output` is the device's reply text (may contain newlines/quotes). 52 + // `ok` is 1 for success, 0 for failure. 53 + void machines_send_response(ACMachines *m, const char *cmd_id, 54 + const char *cmd, const char *output, int ok); 45 55 46 56 // Call at shutdown 47 57 void machines_destroy(ACMachines *m);
+3
session-server/session.mjs
··· 1822 1822 command: msg.cmd, 1823 1823 commandId, 1824 1824 target: msg.args?.target || msg.args?.piece || undefined, 1825 + // Free-text payload for cmd:"prompt" — runs through the 1826 + // device's prompt.mjs execute() exactly as if typed locally. 1827 + text: typeof msg.args?.text === "string" ? msg.args.text : undefined, 1825 1828 })); 1826 1829 log(`Command '${msg.cmd}' → ${msg.machineId} (${commandId})`); 1827 1830 }
+43 -14
system/public/aesthetic.computer/disks/machines.mjs
··· 213 213 logs.unshift(...fetched); 214 214 if (logs.length > 500) logs.length = 500; 215 215 } 216 + } else if (msg.command === "prompt" && msg.data) { 217 + if (selectedMachine?.machineId === msg.machineId) { 218 + const ok = msg.data.ok !== false; 219 + const out = typeof msg.data.output === "string" 220 + ? msg.data.output 221 + : JSON.stringify(msg.data); 222 + out.split("\n").filter(Boolean).forEach((line) => { 223 + logs.unshift({ 224 + level: ok ? "reply" : "error", 225 + message: (ok ? "← " : "✗ ") + line, 226 + type: "prompt", 227 + when: new Date().toISOString(), 228 + }); 229 + }); 230 + if (logs.length > 500) logs.length = 500; 231 + } 216 232 } 217 233 break; 218 234 } ··· 453 469 const btnH = 12; 454 470 455 471 if (jumpInputActive) { 456 - ink(80).write("Piece:", { x: M, y: btnY }); 457 - ink(255).write(jumpText + "_", { x: M + 42, y: btnY }); 472 + ink(80).write("Prompt:", { x: M, y: btnY }); 473 + ink(255).write(jumpText + "_", { x: M + 50, y: btnY }); 458 474 y += btnH + 4; 459 475 } else { 460 - // Jump button 461 - ink(100, 200, 255).write("[Jump]", { x: M, y: btnY }); 476 + // Prompt button — free-text execute through the device's prompt.mjs 477 + ink(100, 200, 255).write("[Prompt]", { x: M, y: btnY }); 462 478 // Update button 463 - ink(100, 220, 100).write("[Update]", { x: M + 44, y: btnY }); 479 + ink(100, 220, 100).write("[Update]", { x: M + 56, y: btnY }); 464 480 // Reboot button 465 - ink(220, 100, 100).write("[Reboot]", { x: M + 100, y: btnY }); 481 + ink(220, 100, 100).write("[Reboot]", { x: M + 112, y: btnY }); 466 482 y += btnH + 4; 467 483 } 468 484 ··· 525 541 ? [220, 80, 80] 526 542 : entry.level === "warn" 527 543 ? [220, 180, 60] 528 - : [70, 70, 90]; 544 + : entry.level === "cmd" 545 + ? [120, 200, 255] 546 + : entry.level === "reply" 547 + ? [140, 220, 140] 548 + : [70, 70, 90]; 529 549 530 550 ink(...levelColor).write(time, { x: M, y: logY }); 531 551 ink(160).write( ··· 597 617 return; 598 618 } 599 619 600 - // Jump text input 620 + // Prompt text input — sends free text through the device's prompt.mjs 601 621 if (jumpInputActive) { 602 622 if (e.is("keyboard:down:enter") && jumpText.length > 0) { 603 - sendCommand(selectedMachine.machineId, "jump", { piece: jumpText }); 623 + sendCommand(selectedMachine.machineId, "prompt", { text: jumpText }); 624 + // Mirror the submission as a log entry so the viewer sees what was typed 625 + // before the device's reply lands a tick later. 626 + logs.unshift({ 627 + level: "cmd", 628 + message: "❯ " + jumpText, 629 + type: "prompt", 630 + when: new Date().toISOString(), 631 + }); 632 + if (logs.length > 500) logs.length = 500; 604 633 jumpInputActive = false; 605 634 jumpText = ""; 606 635 needsPaint(); ··· 667 696 668 697 // Command buttons (at row ~after info) 669 698 // Approximate: commands area starts around y=120-160 depending on hw info 670 - // We use the text positions: [Jump] starts at M, [Update] at M+44, [Reboot] at M+100 699 + // Text positions: [Prompt] starts at M, [Update] at M+56, [Reboot] at M+112. 671 700 const cmdY = findCommandY(); 672 701 if (py >= cmdY && py < cmdY + 14) { 673 - if (px >= M && px < M + 40) { 674 - // Jump 702 + if (px >= M && px < M + 52) { 703 + // Prompt — open free-text input 675 704 jumpInputActive = true; 676 705 jumpText = ""; 677 706 needsPaint(); 678 707 return; 679 708 } 680 - if (px >= M + 44 && px < M + 96) { 709 + if (px >= M + 56 && px < M + 108) { 681 710 // Update 682 711 sendCommand(selectedMachine.machineId, "update"); 683 712 needsPaint(); 684 713 return; 685 714 } 686 - if (px >= M + 100 && px < M + 152) { 715 + if (px >= M + 112 && px < M + 164) { 687 716 // Reboot 688 717 confirmReboot = selectedMachine.machineId; 689 718 needsPaint();