moshcode 0.72.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.72.0",
3
+ "version": "0.73.0",
4
4
  "type": "module",
5
5
  "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {
package/src/mirror.mjs CHANGED
@@ -12,6 +12,47 @@
12
12
  import os from "node:os";
13
13
  import { loadCreds } from "./auth.mjs";
14
14
 
15
+ // A key pressed on the session page travels through the same queue as a typed
16
+ // line, tagged with this sentinel. ESC leads it because it is a byte you cannot
17
+ // put in the send box by typing, so a key can never collide with a real command
18
+ // — and the app refuses to queue one for a mosh too old to decode it, rather
19
+ // than letting the sentinel get typed at somebody's prompt as text.
20
+ export const KEY_PREFIX = "\u001bmoshkey:";
21
+ /** Keys the page can send. Anything else is ignored on both ends. */
22
+ export const KEY_NAMES = ["up", "down", "left", "right", "enter"];
23
+
24
+ /** The name of the key this command carries, or null if it is an ordinary line. */
25
+ export function decodeKey(body) {
26
+ if (typeof body !== "string" || !body.startsWith(KEY_PREFIX)) return null;
27
+ const name = body.slice(KEY_PREFIX.length);
28
+ return KEY_NAMES.includes(name) ? name : null;
29
+ }
30
+
31
+ // What each key looks like to a program reading the tty in raw mode, and the
32
+ // keypress readline wants when it is the one holding the line.
33
+ const KEY_BYTES = { up: "\u001b[A", down: "\u001b[B", right: "\u001b[C", left: "\u001b[D", enter: "\r" };
34
+ const KEY_PRESS = {
35
+ up: { name: "up" }, down: { name: "down" }, right: { name: "right" },
36
+ left: { name: "left" }, enter: { name: "return" },
37
+ };
38
+
39
+ /**
40
+ * Deliver one key to whatever is reading this terminal. Returns false for a key
41
+ * we don't know, so a newer app can never make an older CLI do something odd.
42
+ */
43
+ export function pressKey(name, rl = null, stdin = process.stdin) {
44
+ const bytes = KEY_BYTES[name];
45
+ if (!bytes) return false;
46
+ // At the prompt readline owns the line editor, so hand it a keypress rather
47
+ // than bytes: ↑/↓ walk the history, ←/→ move within the line, enter runs it.
48
+ if (rl) {
49
+ try { rl.write(null, KEY_PRESS[name]); return true; } catch { /* fall through to the tty */ }
50
+ }
51
+ // Otherwise something has the tty in raw mode — a herd bar, the reader, a
52
+ // menu — and it is waiting on the real escape sequence, not on readline.
53
+ try { stdin.emit("data", Buffer.from(bytes, "latin1")); return true; } catch { return false; }
54
+ }
55
+
15
56
  const FLUSH_MS = 150; // batch writes so a busy render is one request, not fifty
16
57
  const MAX_BUFFER = 16000; // flush early once a batch gets big
17
58
 
@@ -32,6 +73,7 @@ export function createMirror({
32
73
  // process alive for up to a full poll window after the pit closes.
33
74
  let poll = null;
34
75
  const onCommand = new Set();
76
+ const onKey = new Set();
35
77
 
36
78
  const api = (creds?.api || "https://app.moshcode.sh").replace(/\/+$/, "");
37
79
  const headers = { "content-type": "application/json", authorization: `Bearer ${creds?.token}` };
@@ -114,7 +156,13 @@ export function createMirror({
114
156
  if (stopped) return;
115
157
  if (!got) { await sleep(5000); continue; }
116
158
  for (const c of got.commands || []) {
117
- for (const fn of onCommand) { try { fn(c.body); } catch { /* handler's problem */ } }
159
+ // A key is a navigation action, not a line, so it goes to its own
160
+ // handlers: the pit presses it the moment it lands instead of parking
161
+ // it behind whatever text is still waiting for the prompt. A "down"
162
+ // delivered a command later would land on a different row.
163
+ const key = decodeKey(c.body);
164
+ if (key) { for (const fn of onKey) { try { fn(key); } catch { /* handler's problem */ } } }
165
+ else { for (const fn of onCommand) { try { fn(c.body); } catch { /* handler's problem */ } } }
118
166
  post(`/api/sessions/${sessionId}/commands/${c.id}`, {});
119
167
  }
120
168
  }
@@ -131,6 +179,10 @@ export function createMirror({
131
179
  host: os.hostname(),
132
180
  version,
133
181
  cwd,
182
+ // What this build can be asked to do. The page arms its arrow pad on
183
+ // the strength of this: a mosh that never says "keys" is one that would
184
+ // type the sentinel at the prompt instead of pressing it.
185
+ features: ["keys"],
134
186
  ...size(),
135
187
  });
136
188
  if (!r?.id) return false;
@@ -146,6 +198,8 @@ export function createMirror({
146
198
  setEngine,
147
199
  /** Subscribe to commands sent from the web. Returns an unsubscribe fn. */
148
200
  onCommand(fn) { onCommand.add(fn); return () => onCommand.delete(fn); },
201
+ /** Subscribe to keys pressed on the web. Returns an unsubscribe fn. */
202
+ onKey(fn) { onKey.add(fn); return () => onKey.delete(fn); },
149
203
  async stop() {
150
204
  if (!sessionId || stopped) return;
151
205
  stopped = true;
package/src/tui.mjs CHANGED
@@ -17,7 +17,7 @@ import { locate, tilde } from "./pwd.mjs";
17
17
  import { createPrd, listPrds, authoringPrompt } from "./prd.mjs";
18
18
  import { loginAuto, whoami, logout } from "./auth.mjs";
19
19
  import { loadCommand, saveCommand } from "./settings-sync.mjs";
20
- import { createMirror, teeOutput } from "./mirror.mjs";
20
+ import { createMirror, pressKey, teeOutput } from "./mirror.mjs";
21
21
  import { fetchMotdAd } from "./ads.mjs";
22
22
  import { runScript } from "./runtime.mjs";
23
23
  import { moshVocabulary } from "./commands.mjs";
@@ -1302,6 +1302,13 @@ async function startMirror() {
1302
1302
  };
1303
1303
  mirror.onCommand((body) => { queue.push(body); drainRemote(); });
1304
1304
 
1305
+ // Keys skip the queue: they are pressed the instant they arrive, whether the
1306
+ // prompt is armed or something else has the tty (a herd bar, the reader, a
1307
+ // menu). Nothing is echoed for them either — a line gets a `▸ (web)` note
1308
+ // because it would otherwise appear from nowhere, but a key's effect is the
1309
+ // redraw it causes, and printing over that would shift it out of place.
1310
+ mirror.onKey((name) => { pressKey(name, promptRl); });
1311
+
1305
1312
  console.log(info(`mirroring this session → ${acid(mirror.url)}`));
1306
1313
  return { restoreTee, drainRemote, atPrompt: (rl) => { promptRl = rl; } };
1307
1314
  }