oh-my-agent-web 0.2.0 → 0.3.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/README.md CHANGED
@@ -171,6 +171,12 @@ oh-my-agent-web ws -t <threadId> "continue" # resume an existing thread
171
171
  oh-my-agent-web ws -y --model gpt-5.5 "..." # auto-approve approvals, pick a model
172
172
  ```
173
173
 
174
+ Interactive mode prints the active model on connect and shows it in the prompt; use
175
+ `/model` to pick from the server's models with ↑/↓ + Enter (or `/model <name>` to switch
176
+ directly; applies from the next turn), `/help` for commands and `/quit` to exit. A turn
177
+ that fails with the upstream "prompt flagged" false-positive is retried automatically
178
+ once (`--retry <n>` / `--no-retry`).
179
+
174
180
  Run the same client through npm (pass flags after `--`):
175
181
 
176
182
  ```bash
@@ -185,11 +191,13 @@ npm run ws -- -t <threadId> "continue"
185
191
  | `--addr <host:port>` / `--host <h>` / `-p, --port <n>` | Address or port (default `127.0.0.1:25258`) |
186
192
  | `--cwd <dir>` | Working directory (default: current dir) |
187
193
  | `-t, --thread <id>` | Resume an existing thread |
188
- | `--model <name>` | Model id |
194
+ | `--model <name>` | Model id (`/model` in interactive mode to list/switch) |
189
195
  | `--sandbox <mode>` / `--approval <mode>` | Session sandbox / approval policy |
190
196
  | `-y, --auto-approve` / `--decline` | Auto-answer approvals instead of prompting |
191
197
  | `-v, --verbose` / `--json` | Print reasoning deltas / raw event JSON |
192
198
  | `--timeout <sec>` | Max wait per turn (default 1800) |
199
+ | `--retry <n>` / `--no-retry` | Retry a flagged/transient turn up to n times (default 1) |
200
+ | `--retry-delay <ms>` | Base retry delay, default 1000 (grows per attempt) |
193
201
 
194
202
  Env: `CODEX_WS_URL` / `CODEX_WS_HOST` / `CODEX_WS_PORT`.
195
203
 
@@ -36,7 +36,7 @@ Connection:
36
36
  Session:
37
37
  --cwd <dir> Working directory (default: current dir)
38
38
  -t, --thread <id> Resume an existing thread
39
- --model <name> Model id
39
+ --model <name> Model id (use /model interactively to list/switch)
40
40
  --sandbox <mode> read-only | workspace-write | danger-full-access
41
41
  --approval <mode> untrusted | on-request | on-failure | never
42
42
  -y, --auto-approve Auto-accept approvals (acceptForSession)
@@ -44,7 +44,15 @@ Session:
44
44
  -v, --verbose Print reasoning deltas
45
45
  --json Print raw event JSON only
46
46
  --timeout <sec> Max wait for a turn, default 1800
47
+ --retry <n> Retry a flagged/transient turn up to n times, default 1 (0 = off)
48
+ --retry-delay <ms> Base delay between retries, default 1000 (grows per attempt)
47
49
  -h, --help Show this help
50
+
51
+ Interactive commands (type at the prompt):
52
+ /model Pick a model (↑/↓ + Enter), or list available ones
53
+ /model <name> Switch the model for subsequent turns
54
+ /help Print this help
55
+ /quit Exit
48
56
  `;
49
57
 
50
58
  /** Parse `oh-my-agent-web ws [ ... ]` arguments. */
@@ -65,6 +73,8 @@ export function parseWsArgs(argv) {
65
73
  verbose: false,
66
74
  json: false,
67
75
  timeout: 1800,
76
+ retry: 1,
77
+ retryDelayMs: 1000,
68
78
  messages: [],
69
79
  help: false,
70
80
  };
@@ -131,6 +141,15 @@ export function parseWsArgs(argv) {
131
141
  case "--timeout":
132
142
  opts.timeout = Number(next());
133
143
  break;
144
+ case "--retry":
145
+ opts.retry = Number(next());
146
+ break;
147
+ case "--no-retry":
148
+ opts.retry = 0;
149
+ break;
150
+ case "--retry-delay":
151
+ opts.retryDelayMs = Number(next());
152
+ break;
134
153
  case "-h":
135
154
  case "--help":
136
155
  opts.help = true;
@@ -384,9 +403,24 @@ export async function runWsClient(argv) {
384
403
  : await client.request("thread/start", threadParams);
385
404
 
386
405
  const threadId = threadResp.thread.id;
406
+ let currentModel = threadResp.model ?? opts.model ?? null;
387
407
  log(
388
- `[thread] ${threadId} model=${threadResp.model} cwd=${threadResp.cwd} sandbox=${JSON.stringify(threadResp.sandbox)}`,
408
+ `[thread] ${threadId} cwd=${threadResp.cwd} sandbox=${JSON.stringify(threadResp.sandbox)}`,
389
409
  );
410
+ log(`[model] ${currentModel ?? "(server default)"} \x1b[2m(/model to list or switch)\x1b[0m`);
411
+
412
+ /** Lazily fetch the server's model catalog (id / model / displayName / …). */
413
+ let modelsCache = null;
414
+ async function loadModels() {
415
+ if (modelsCache) return modelsCache;
416
+ try {
417
+ const resp = await client.request("model/list", {});
418
+ modelsCache = Array.isArray(resp?.data) ? resp.data : [];
419
+ } catch {
420
+ modelsCache = [];
421
+ }
422
+ return modelsCache;
423
+ }
390
424
 
391
425
  /* -------- event rendering -------- */
392
426
  let inAgentMessage = false;
@@ -503,20 +537,164 @@ export async function runWsClient(argv) {
503
537
  decide(id, "decline");
504
538
  });
505
539
 
506
- async function ask(text) {
507
- const done = new Promise((resolve) => client.on("turnDone", resolve, { once: true }));
540
+ /** Run one turn to completion and return its final state. */
541
+ async function runTurn(text) {
542
+ const done = new Promise((resolve) => client.on("turnDone", (ev) => resolve(ev.detail), { once: true }));
508
543
  const started = await client.request("turn/start", {
509
544
  threadId,
510
545
  input: [{ type: "text", text, text_elements: [] }],
546
+ ...(currentModel ? { model: currentModel } : {}),
511
547
  });
512
548
  activeTurn = { threadId, turnId: started?.turn?.id ?? null };
513
549
  const timer = setTimeout(() => {
514
550
  log(`\x1b[31m[timeout] turn did not finish within ${opts.timeout}s\x1b[0m`);
515
551
  client.emit("turnDone", { status: "timeout" });
516
552
  }, opts.timeout * 1000);
517
- await done;
553
+ const turn = await done;
518
554
  activeTurn = null;
519
555
  clearTimeout(timer);
556
+ return turn;
557
+ }
558
+
559
+ /**
560
+ * A failed turn worth retrying. The upstream moderation layer sometimes flags
561
+ * a perfectly innocent prompt ("Invalid prompt … potentially violating our
562
+ * usage policy") and codex reports willRetry:false, so without this the turn is
563
+ * simply lost. Re-sending the same input almost always succeeds.
564
+ */
565
+ function isRetryableTurn(turn) {
566
+ if (!turn || turn.status !== "failed") return false;
567
+ const msg = String(turn.error?.message ?? "");
568
+ return /flagged as potentially violating|Invalid prompt/i.test(msg);
569
+ }
570
+
571
+ async function ask(text) {
572
+ const maxRetries = Math.max(0, Math.floor(Number(opts.retry)) || 0);
573
+ const baseDelay = Math.max(0, Number(opts.retryDelayMs) || 0);
574
+ for (let attempt = 0; ; attempt++) {
575
+ const turn = await runTurn(text);
576
+ if (attempt >= maxRetries || !isRetryableTurn(turn)) return;
577
+ const wait = baseDelay * (attempt + 1);
578
+ log(
579
+ `\x1b[33m[retry] retryable failure — retrying in ${wait}ms (attempt ${attempt + 2}/${maxRetries + 1})\x1b[0m`,
580
+ );
581
+ await new Promise((r) => setTimeout(r, wait));
582
+ }
583
+ }
584
+
585
+ /** Arrow-key model chooser used by `/model`. Returns a chosen id, or null. */
586
+ async function pickModel(models, activeId) {
587
+ const rows = models.map((m) => {
588
+ const id = m.id ?? m.model;
589
+ const label = m.displayName && m.displayName !== id ? ` \x1b[2m${m.displayName}\x1b[0m` : "";
590
+ return { id, text: `${id}${label}` };
591
+ });
592
+ if (!process.stdin.isTTY) {
593
+ log(" available:");
594
+ for (const r of rows) log(` ${r.text}`);
595
+ log(" pick one with `/model <name>`");
596
+ return null;
597
+ }
598
+ let index = Math.max(0, rows.findIndex((r) => r.id === activeId));
599
+ const total = rows.length;
600
+ const draw = (initial) => {
601
+ if (!initial) process.stderr.write(`\x1b[${total + 1}A`);
602
+ for (let i = 0; i < total; i++) {
603
+ const pointer = i === index ? "\x1b[36m›\x1b[0m" : " ";
604
+ const star = rows[i].id === activeId ? " \x1b[32m*\x1b[0m" : "";
605
+ process.stderr.write(`\x1b[2K${pointer} ${rows[i].text}${star}\n`);
606
+ }
607
+ process.stderr.write("\x1b[2K\x1b[2m ↑/↓ move · Enter select · Esc cancel\x1b[0m\n");
608
+ };
609
+ log("[model] select a model:");
610
+ draw(true);
611
+ return await new Promise((resolve) => {
612
+ const stdin = process.stdin;
613
+ const wasRaw = Boolean(stdin.isRaw);
614
+ rl?.pause();
615
+ if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
616
+ stdin.resume();
617
+ const finish = (value) => {
618
+ stdin.removeListener("data", onData);
619
+ if (typeof stdin.setRawMode === "function") stdin.setRawMode(wasRaw);
620
+ stdin.pause();
621
+ rl?.resume();
622
+ resolve(value);
623
+ };
624
+ const onData = (buf) => {
625
+ const s = buf.toString("utf8");
626
+ if (s === "\u0003") {
627
+ finish(null);
628
+ process.kill(process.pid, "SIGINT");
629
+ } else if (s === "\u001b[A" || s === "k") {
630
+ index = (index - 1 + total) % total;
631
+ draw();
632
+ } else if (s === "\u001b[B" || s === "j") {
633
+ index = (index + 1) % total;
634
+ draw();
635
+ } else if (s === "\r" || s === "\n") {
636
+ finish(rows[index].id);
637
+ } else if (s === "\u001b" || s === "q") {
638
+ finish(null);
639
+ } else if (/^[1-9]$/.test(s)) {
640
+ const n = Number(s) - 1;
641
+ if (n < total) {
642
+ index = n;
643
+ draw();
644
+ }
645
+ }
646
+ };
647
+ stdin.on("data", onData);
648
+ });
649
+ }
650
+
651
+ /** Interactive slash commands (/model, /help, /quit). */
652
+ async function handleCommand(line) {
653
+ const [cmd, ...rest] = line.slice(1).trim().split(/\s+/);
654
+ switch (cmd) {
655
+ case "help":
656
+ process.stdout.write(HELP);
657
+ return true;
658
+ case "quit":
659
+ case "exit":
660
+ await shutdown(0);
661
+ return true;
662
+ case "model":
663
+ case "models": {
664
+ const models = await loadModels();
665
+ if (!rest.length) {
666
+ log(`[model] current: ${currentModel ?? "(server default)"}`);
667
+ if (!models.length) {
668
+ log(" (this server exposes no model list)");
669
+ return true;
670
+ }
671
+ const picked = await pickModel(models, currentModel);
672
+ if (picked && picked !== currentModel) {
673
+ currentModel = picked;
674
+ log(`[model] switched to ${currentModel} \x1b[2m(applies from the next turn)\x1b[0m`);
675
+ updatePrompt();
676
+ }
677
+ return true;
678
+ }
679
+ const wanted = rest[0];
680
+ const hit = models.find((m) => m.id === wanted || m.model === wanted || m.displayName === wanted);
681
+ if (models.length && !hit) {
682
+ log(`\x1b[31m[model] unknown model "${wanted}" — run /model to list\x1b[0m`);
683
+ return true;
684
+ }
685
+ currentModel = hit ? (hit.id ?? hit.model) : wanted;
686
+ log(`[model] switched to ${currentModel} \x1b[2m(applies from the next turn)\x1b[0m`);
687
+ updatePrompt();
688
+ return true;
689
+ }
690
+ default:
691
+ log(`unknown command: /${cmd} — try /help`);
692
+ return true;
693
+ }
694
+ }
695
+
696
+ function updatePrompt() {
697
+ if (rl && process.stdin.isTTY) rl.setPrompt(`\x1b[2m${currentModel ?? "model?"}\x1b[0m \x1b[36m›\x1b[0m `);
520
698
  }
521
699
 
522
700
  function setupRepl() {
@@ -527,10 +705,20 @@ export async function runWsClient(argv) {
527
705
  if (process.stdin.isTTY) rl.prompt();
528
706
  return;
529
707
  }
708
+ if (text.startsWith("/")) {
709
+ try {
710
+ await handleCommand(text);
711
+ } catch (e) {
712
+ log(`\x1b[31m[error] ${e.message}\x1b[0m`);
713
+ }
714
+ if (process.stdin.isTTY) rl.prompt();
715
+ return;
716
+ }
530
717
  if (pendingApproval) {
531
718
  if (/^(y|yes)$/i.test(text)) decide(pendingApproval.id, "accept");
532
719
  else if (/^(a|all)$/i.test(text)) decide(pendingApproval.id, "acceptForSession");
533
720
  else decide(pendingApproval.id, "decline");
721
+ updatePrompt();
534
722
  if (process.stdin.isTTY) rl.prompt();
535
723
  return;
536
724
  }
@@ -548,7 +736,8 @@ export async function runWsClient(argv) {
548
736
  void shutdown(0);
549
737
  });
550
738
  if (process.stdin.isTTY) {
551
- log("\ntype a message and press Enter (Ctrl-C: interrupt & close / Ctrl-D: exit). approvals: y=once / a=session / n=decline\n");
739
+ updatePrompt();
740
+ log("\ntype a message and press Enter (Ctrl-C: interrupt & close / Ctrl-D: exit). approvals: y=once / a=session / n=decline\n /model to list or switch models · /help for all commands\n");
552
741
  rl.prompt();
553
742
  }
554
743
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-agent-web",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Browser cockpit for the Codex CLI app-server — chat, tool calls, diffs and approvals in one tab",
5
5
  "license": "MIT",
6
6
  "type": "module",