@runuai/host 0.8.34 → 0.8.36

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/ui/app.js CHANGED
@@ -7,7 +7,7 @@ const $ = (id) => document.getElementById(id);
7
7
  const expanded = new Set(); // task ids whose events are expanded (persist across polls)
8
8
  let latestTasks = [];
9
9
  let latestEvents = [];
10
- let latestEngines = null; // { catalog, statuses } from /api/engines
10
+ let latestEngines = null; // { catalog, statuses, cli, accounts } from /api/engines
11
11
 
12
12
  const KIND_CLASS = {
13
13
  "task.created": "created",
@@ -300,6 +300,16 @@ function engineCard(e) {
300
300
  head.append(meta);
301
301
  card.append(head);
302
302
 
303
+ // ADR-076: multiple accounts per engine. Rendered for account-model kinds
304
+ // (claude/codex/opencode). Extra accounts can be removed; claude/codex can
305
+ // add one by pasting a token / API key.
306
+ const accounts = (latestEngines?.accounts || {})[e.kind] || [];
307
+ const extras = accounts.filter((a) => !a.isDefault);
308
+ const canAdd = e.kind === "claude" || e.kind === "codex";
309
+ if (accounts.length > 0 && (extras.length > 0 || canAdd)) {
310
+ card.append(engineAccounts(e, accounts, canAdd));
311
+ }
312
+
303
313
  const btn = document.createElement("button");
304
314
  btn.className = "link-btn danger";
305
315
  btn.type = "button";
@@ -318,6 +328,75 @@ function engineCard(e) {
318
328
  return card;
319
329
  }
320
330
 
331
+ /** Accounts sub-panel for an engine card (ADR-076). */
332
+ function engineAccounts(e, accounts, canAdd) {
333
+ const box = document.createElement("div");
334
+ box.className = "engine-accounts";
335
+
336
+ const title = document.createElement("div");
337
+ title.className = "engine-sub";
338
+ title.textContent = `Accounts (${accounts.length}) — rotated automatically`;
339
+ box.append(title);
340
+
341
+ for (const a of accounts) {
342
+ const row = document.createElement("div");
343
+ row.className = "engine-account-row";
344
+ const name = document.createElement("span");
345
+ name.textContent = a.isDefault ? `${a.label} (default)` : a.label;
346
+ row.append(name);
347
+ if (!a.isDefault) {
348
+ const rm = document.createElement("button");
349
+ rm.className = "link-btn danger";
350
+ rm.type = "button";
351
+ rm.textContent = "Remove";
352
+ rm.addEventListener("click", async () => {
353
+ rm.disabled = true;
354
+ try {
355
+ await postJSON("/api/engines/accounts/remove", { id: a.id });
356
+ } catch {
357
+ /* poll re-syncs truth */
358
+ }
359
+ await poll();
360
+ });
361
+ row.append(rm);
362
+ }
363
+ box.append(row);
364
+ }
365
+
366
+ if (canAdd) {
367
+ const add = document.createElement("button");
368
+ add.className = "link-btn";
369
+ add.type = "button";
370
+ add.textContent = "+ Add account";
371
+ add.addEventListener("click", () => void addAccountFlow(e));
372
+ box.append(add);
373
+ }
374
+ return box;
375
+ }
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 =
388
+ 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();
398
+ }
399
+
321
400
  // --- add-engine modal -------------------------------------------------------
322
401
 
323
402
  function openModal() {
@@ -386,6 +465,8 @@ function setupEngine(e) {
386
465
 
387
466
  if (e.authMode === "api-key") {
388
467
  body.append(apiKeyForm(e));
468
+ } else if (e.authMode === "external-login") {
469
+ body.append(externalLoginForm(e));
389
470
  } else {
390
471
  body.append(commandForm(e));
391
472
  }
@@ -465,6 +546,114 @@ function apiKeyForm(e) {
465
546
  return form;
466
547
  }
467
548
 
549
+ /**
550
+ * external-login mode (OpenCode): the engine's own CLI is an interactive TUI we
551
+ * can't drive here, so the owner runs it in their terminal and we just detect
552
+ * the credential it wrote. Install (if the CLI is missing) → run the login
553
+ * command → "Check connection".
554
+ */
555
+ function externalLoginForm(e) {
556
+ const form = document.createElement("div");
557
+ form.className = "engine-setup";
558
+
559
+ if (e.notes) {
560
+ const note = document.createElement("p");
561
+ note.className = "setup-note";
562
+ note.textContent = e.notes;
563
+ form.append(note);
564
+ }
565
+
566
+ const status = document.createElement("div");
567
+ status.className = "setup-status";
568
+ const log = document.createElement("pre");
569
+ log.className = "log";
570
+ log.hidden = true;
571
+ const pushLine = (line) => {
572
+ log.textContent += (log.textContent ? "\n" : "") + line;
573
+ log.scrollTop = log.scrollHeight;
574
+ };
575
+
576
+ const cliMissing =
577
+ !!latestEngines?.cli && latestEngines.cli[e.kind] === false && !!e.installHint;
578
+
579
+ // Step 1 (only when the CLI is absent): install it.
580
+ const installWrap = document.createElement("div");
581
+ if (cliMissing) {
582
+ const installNote = document.createElement("p");
583
+ installNote.className = "setup-note";
584
+ installNote.textContent = `The ${e.label} CLI isn't installed on this Mac. Install it with:`;
585
+ const installCmd = document.createElement("pre");
586
+ installCmd.className = "log";
587
+ installCmd.textContent = e.installHint;
588
+ const install = document.createElement("button");
589
+ install.className = "btn";
590
+ install.type = "button";
591
+ install.textContent = "Install";
592
+ install.addEventListener("click", async () => {
593
+ install.disabled = true;
594
+ install.textContent = "Installing…";
595
+ status.className = "setup-status";
596
+ status.textContent = "Running the installer…";
597
+ log.hidden = false;
598
+ log.textContent = "";
599
+ const result = await runInstall({ kind: e.kind }, pushLine);
600
+ if (result.ok) {
601
+ await poll();
602
+ installWrap.hidden = true;
603
+ status.className = "setup-status";
604
+ status.textContent = "Installed. Now sign in from your terminal below.";
605
+ } else {
606
+ install.disabled = false;
607
+ install.textContent = "Try again";
608
+ status.className = "setup-status err";
609
+ status.textContent = result.message || "Install failed.";
610
+ }
611
+ });
612
+ installWrap.append(installNote, installCmd, install);
613
+ } else {
614
+ installWrap.hidden = true;
615
+ }
616
+ form.append(installWrap);
617
+
618
+ // Step 2: the command the owner runs in their own terminal.
619
+ if (e.loginCmd) {
620
+ const runNote = document.createElement("p");
621
+ runNote.className = "setup-note";
622
+ runNote.textContent = "Run this in your terminal to sign in:";
623
+ const cmd = document.createElement("pre");
624
+ cmd.className = "log";
625
+ cmd.textContent = e.loginCmd;
626
+ form.append(runNote, cmd);
627
+ }
628
+
629
+ // Step 3: re-detect what login wrote.
630
+ const actions = document.createElement("div");
631
+ actions.className = "setup-actions";
632
+ const check = document.createElement("button");
633
+ check.className = "btn";
634
+ check.type = "button";
635
+ check.textContent = "Check connection";
636
+ check.addEventListener("click", async () => {
637
+ check.disabled = true;
638
+ check.textContent = "Checking…";
639
+ status.className = "setup-status";
640
+ status.textContent = "Looking for your credentials…";
641
+ const result = await runConnect({ kind: e.kind });
642
+ if (result.ok) {
643
+ await poll();
644
+ closeModal();
645
+ } else {
646
+ check.disabled = false;
647
+ check.textContent = "Check again";
648
+ status.className = "setup-status err";
649
+ status.textContent = result.message || "Not signed in yet.";
650
+ }
651
+ });
652
+ actions.append(check);
653
+ form.append(actions, status, log);
654
+ return form;
655
+ }
656
+
468
657
  /** token/login mode: a Connect button + a live log; Claude adds a paste link. */
469
658
  function commandForm(e) {
470
659
  const form = document.createElement("div");
package/ui/style.css CHANGED
@@ -377,12 +377,31 @@ code,
377
377
  .engine-card {
378
378
  display: flex;
379
379
  align-items: center;
380
+ flex-wrap: wrap;
380
381
  gap: 0.75rem;
381
382
  border: 1px solid var(--line);
382
383
  border-radius: var(--radius);
383
384
  padding: 0.6rem 0.9rem;
384
385
  }
385
386
 
387
+ /* ADR-076: accounts sub-panel spans the full card width below the head/actions. */
388
+ .engine-accounts {
389
+ flex-basis: 100%;
390
+ border-top: 1px solid var(--line);
391
+ padding-top: 0.5rem;
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: 0.35rem;
395
+ }
396
+
397
+ .engine-account-row {
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: space-between;
401
+ gap: 0.75rem;
402
+ font-size: 0.88rem;
403
+ }
404
+
386
405
  .engine-head {
387
406
  display: flex;
388
407
  align-items: center;