@khanglvm/relay 0.13.0 → 0.13.1

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": "@khanglvm/relay",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Question boards with rich blocks (markdown, charts, mermaid, tables, code, diffs, video, sandboxed HTML), clickable local file-links, and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, read answers as JSON — in a local browser board OR rendered INLINE inside the Claude & Codex apps as an MCP App (SEP-1865).",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -472,9 +472,31 @@ one yesno "Does this match your mental model?" + a textarea for notes.
472
472
  `rly history` (saved boards) · `rly spec <id>` (print spec to modify) ·
473
473
  `rly reuse <id>` (re-run blank) · `rly reopen <id>` (re-open with saved
474
474
  answers prefilled) · `rly reopen <id> --replies file.json` (add agent replies) ·
475
+ `rly rescue <id>` (re-serve a dropped board on its ORIGINAL port) ·
475
476
  `rly list` / `rly open` / `rly stop <id>` · `rly rm <id>`.
476
477
  Multiple boards can run concurrently.
477
478
 
479
+ ### Continue a board — reconnect, NEVER recreate it
480
+
481
+ When the user refers to a board that already exists — a URL/port ("the board on
482
+ `127.0.0.1:59926`"), "the board from yesterday", "reopen it", "it disconnected"
483
+ — do **NOT** run `rly ask`/`rly show`. A fresh board lands on a **new port**,
484
+ **strands the user's open tab** on the dead one, and **loses their comments**.
485
+ Find the real board and reconnect it:
486
+
487
+ 1. **Identify it** — `rly list` (running) and `rly history` (saved) print each
488
+ board's id, title, and url/port. Match by what the user said (port, title).
489
+ 2. **Tab still open but "connection lost"** (server died / machine slept) →
490
+ `rly rescue <id>`. Re-serves on the SAME port so that tab reconnects on its
491
+ own and re-flushes any comments it buffered — no new tab, no lost input.
492
+ 3. **Want a fresh tab** with prior answers prefilled → `rly reopen <id>` (also
493
+ reuses the board's last port, so an old tab still reconnects).
494
+ 4. `rly reuse <id>` is the ONLY "make a new board from this one" path — use it
495
+ solely for a deliberately blank re-run, never to "continue" or "reconnect".
496
+
497
+ Rule of thumb: **an existing board is reconnected (`rescue`/`reopen`), never
498
+ re-asked.** Only call `rly ask`/`rly show` for a genuinely new question.
499
+
478
500
  ### Live mutation — `rly update`
479
501
 
480
502
  Push a new spec to a running board. The page reloads and prefills answers from the
package/src/cli.js CHANGED
@@ -349,7 +349,10 @@ async function cmdReopen(args) {
349
349
  printJson({ status: 'open', boardId: record.id, url: running.url, note: 'already running — browser re-opened' });
350
350
  return 0;
351
351
  }
352
- return runOrDetach(record, args);
352
+ // Reuse the board's last port (unless the user forced one) so a tab still open
353
+ // from a previous run reconnects on its own instead of being stranded on a
354
+ // dead port. runBoard falls back to a random port if it's been taken.
355
+ return runOrDetach(record, { ...args, port: args.port ?? record.lastPort });
353
356
  }
354
357
 
355
358
  // Rescue a board whose browser tab is still open but disconnected (its server
package/src/server.js CHANGED
@@ -693,9 +693,25 @@ export async function runBoard({ id, port = 0, open = true, timeoutSec = 1800, q
693
693
  }
694
694
  });
695
695
 
696
+ // Bind the requested port (reopen/rescue reuse the board's last port so a
697
+ // still-open tab reconnects). If that port was grabbed by another process in
698
+ // the meantime, fall back to a random free one rather than failing to boot.
696
699
  await new Promise((resolve, reject) => {
697
- server.once('error', reject);
698
- server.listen(port, '127.0.0.1', resolve);
700
+ const bind = (p, allowFallback) => {
701
+ const onErr = (e) => {
702
+ if (allowFallback && e && e.code === 'EADDRINUSE' && p !== 0) {
703
+ bind(0, false); // desired port busy → random free one
704
+ } else {
705
+ reject(e);
706
+ }
707
+ };
708
+ server.once('error', onErr);
709
+ server.listen(p, '127.0.0.1', () => {
710
+ server.removeListener('error', onErr);
711
+ resolve();
712
+ });
713
+ };
714
+ bind(port, true);
699
715
  });
700
716
  const actualPort = server.address().port;
701
717
  const url = `http://127.0.0.1:${actualPort}/`;