@runuai/host 0.8.37 → 0.8.38

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/ui/app.js +108 -21
  3. package/ui/style.css +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.8.37",
3
+ "version": "0.8.38",
4
4
  "description": "Uai host — runs ephemeral AI coding tasks in Docker on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Diogo Perillo <diogo.perillo@gmail.com>",
package/ui/app.js CHANGED
@@ -368,33 +368,120 @@ function engineAccounts(e, accounts, canAdd) {
368
368
  add.className = "link-btn";
369
369
  add.type = "button";
370
370
  add.textContent = "+ Add account";
371
- add.addEventListener("click", () => void addAccountFlow(e));
371
+ add.addEventListener("click", () => openAddAccount(e));
372
372
  box.append(add);
373
373
  }
374
374
  return box;
375
375
  }
376
376
 
377
- /** Minimal add-account flow: prompt for a label + a token/API key, then POST. */
378
- async function addAccountFlow(e) {
379
- const label = window.prompt(`Label for the new ${e.label} account (e.g. "work"):`);
380
- if (!label) return;
381
- const secretPrompt =
382
- e.kind === "claude"
383
- ? "Paste a Claude token (run `claude setup-token`) or an API key:"
384
- : "Paste an OpenAI API key:";
385
- const secret = window.prompt(secretPrompt);
386
- if (!secret) return;
387
- const body =
377
+ /**
378
+ * Add-account modal (ADR-076). Replaces the old back-to-back window.prompt()
379
+ * flow: a single-line prompt hid what you pasted and silently truncated long
380
+ * `claude setup-token` values, which then surfaced downstream as a 401 on the
381
+ * rotated account. A real form (visible multi-line textarea + inline
382
+ * validation) makes a bad paste obvious before it's saved.
383
+ */
384
+ function openAddAccount(e) {
385
+ $("engine-modal-title").textContent = `Add ${e.label} account`;
386
+ const body = $("engine-modal-body");
387
+ body.replaceChildren();
388
+ body.append(addAccountForm(e));
389
+ openModal();
390
+ }
391
+
392
+ function addAccountForm(e) {
393
+ const form = document.createElement("div");
394
+ form.className = "engine-setup";
395
+
396
+ const note = document.createElement("p");
397
+ note.className = "setup-note";
398
+ note.textContent =
388
399
  e.kind === "claude"
389
- ? { kind: e.kind, label, token: secret }
390
- : { kind: e.kind, label, apiKey: secret };
391
- try {
392
- const res = await postJSON("/api/engines/accounts/add", body);
393
- if (res && res.ok === false && res.message) window.alert(res.message);
394
- } catch {
395
- /* poll re-syncs truth */
396
- }
397
- await poll();
400
+ ? "Paste a token from `claude setup-token` (a long value, usually starting with sk-ant-oat…) or an API key. This account rotates in automatically when another Claude account hits its limit."
401
+ : "Paste an OpenAI API key (starts with sk-…). This account rotates in automatically when another account of this kind hits its limit.";
402
+ form.append(note);
403
+
404
+ const labelField = document.createElement("div");
405
+ labelField.className = "field";
406
+ const labelInput = document.createElement("input");
407
+ labelInput.type = "text";
408
+ labelInput.className = "text-input";
409
+ labelInput.placeholder = "Label (e.g. work)";
410
+ labelInput.autocomplete = "off";
411
+ labelInput.spellcheck = false;
412
+ labelField.append(labelInput);
413
+ form.append(labelField);
414
+
415
+ const secretField = document.createElement("div");
416
+ secretField.className = "field";
417
+ const secretInput = document.createElement("textarea");
418
+ secretInput.className = "text-input token-area";
419
+ secretInput.rows = 3;
420
+ secretInput.placeholder =
421
+ e.kind === "claude" ? "Claude token or API key" : "OpenAI API key";
422
+ secretInput.autocomplete = "off";
423
+ secretInput.spellcheck = false;
424
+ secretField.append(secretInput);
425
+ form.append(secretField);
426
+
427
+ const status = document.createElement("div");
428
+ status.className = "setup-status";
429
+ form.append(status);
430
+
431
+ const fail = (msg) => {
432
+ status.className = "setup-status err";
433
+ status.textContent = msg;
434
+ };
435
+
436
+ const actions = document.createElement("div");
437
+ actions.className = "setup-actions";
438
+ const save = document.createElement("button");
439
+ save.className = "btn";
440
+ save.type = "button";
441
+ save.textContent = "Add account";
442
+ save.addEventListener("click", async () => {
443
+ const label = labelInput.value.trim();
444
+ const secret = secretInput.value.trim();
445
+ if (!label) return fail("Give the account a label.");
446
+ if (!secret) return fail("Paste a token or API key.");
447
+ // Mirrors the host-side check — a token with internal whitespace/newlines is
448
+ // a mangled paste, not a value; catch it here so it never becomes a 401.
449
+ if (/\s/.test(secret)) {
450
+ return fail("That value has spaces or line breaks — paste just the token.");
451
+ }
452
+ save.disabled = true;
453
+ save.textContent = "Adding…";
454
+ const payload =
455
+ e.kind === "claude"
456
+ ? { kind: e.kind, label, token: secret }
457
+ : { kind: e.kind, label, apiKey: secret };
458
+ let result;
459
+ try {
460
+ result = await postJSON("/api/engines/accounts/add", payload);
461
+ } catch {
462
+ result = { ok: false, message: "Couldn't reach the host." };
463
+ }
464
+ if (result && result.ok) {
465
+ await poll();
466
+ closeModal();
467
+ return;
468
+ }
469
+ save.disabled = false;
470
+ save.textContent = "Add account";
471
+ fail((result && result.message) || "Couldn't add the account.");
472
+ });
473
+ actions.append(save);
474
+
475
+ const cancel = document.createElement("button");
476
+ cancel.className = "link-btn";
477
+ cancel.type = "button";
478
+ cancel.textContent = "Cancel";
479
+ cancel.addEventListener("click", closeModal);
480
+ actions.append(cancel);
481
+
482
+ form.append(actions);
483
+ setTimeout(() => labelInput.focus(), 0);
484
+ return form;
398
485
  }
399
486
 
400
487
  // --- add-engine modal -------------------------------------------------------
package/ui/style.css CHANGED
@@ -604,6 +604,16 @@ code,
604
604
  border-color: var(--fg-muted);
605
605
  }
606
606
 
607
+ /* Add-account token field: long secrets must be fully visible (no truncation)
608
+ and wrap so a bad paste is obvious before saving. */
609
+ textarea.token-area {
610
+ resize: vertical;
611
+ min-height: 3.2rem;
612
+ line-height: 1.35;
613
+ word-break: break-all;
614
+ white-space: pre-wrap;
615
+ }
616
+
607
617
  .setup-actions {
608
618
  display: flex;
609
619
  align-items: center;