@monster-orange/den 0.4.1 → 0.4.3

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 (36) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +88 -158
  3. package/docs/distributions.md +151 -28
  4. package/docs/reference.md +177 -0
  5. package/package.json +1 -1
  6. package/src/shared/agents/den-claude.ts +57 -12
  7. package/src/shared/agents/generic.ts +1 -0
  8. package/src/shared/agents/runtime-search.ts +416 -0
  9. package/src/shared/agents/runtime.ts +26 -1
  10. package/src/shared/agents/runtimes.ts +30 -3
  11. package/src/shared/den-config.ts +88 -3
  12. package/src/shared/edge/render.ts +31 -0
  13. package/src/shared/sessions/client.ts +7 -0
  14. package/src/shared/sessions/den-pty.ts +10 -2
  15. package/src/shared/sessions/host.ts +33 -8
  16. package/src/shared/sessions/types.ts +8 -0
  17. package/src/ui/cli/agent-ops.ts +109 -21
  18. package/src/ui/cli/agents-command.ts +100 -4
  19. package/src/ui/cli/index.ts +33 -1
  20. package/src/ui/cli/sessions-command.ts +26 -9
  21. package/src/ui/cli/web-command.ts +60 -9
  22. package/src/ui/server/agent-routes.ts +11 -3
  23. package/src/ui/server/edge-exposure.ts +143 -0
  24. package/src/ui/server/index.ts +105 -23
  25. package/src/ui/server/public/agent-start.js +202 -0
  26. package/src/ui/server/public/agents-page.js +90 -18
  27. package/src/ui/server/public/agents-section.js +6 -18
  28. package/src/ui/server/public/agents.html +70 -0
  29. package/src/ui/server/public/i18n/es.js +11 -0
  30. package/src/ui/server/public/i18n/ja.js +11 -0
  31. package/src/ui/server/public/local-dev.html +43 -14
  32. package/src/ui/server/public/settings-registry.js +1 -1
  33. package/src/ui/server/public/shell.js +40 -0
  34. package/src/ui/server/public/topbar.js +127 -28
  35. package/src/ui/server/request-guard.ts +122 -7
  36. package/src/ui/server/ui-assets.ts +2 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The Start agent form's choices (AG-10.0), shared by the Agents tab's
3
+ * dialog and the Workspaces page's agents section. DOM-free.
4
+ *
5
+ * A persona option shows its id only: its role sentence is the help line
6
+ * under the select (personaHelp), so a long role can't widen the select
7
+ * past the dialog. The runtime's first option names what it resolves to,
8
+ * "Claude Code (persona default)", from the persona picked; a runtime that
9
+ * can't run here says why, and one that can shows its version.
10
+ *
11
+ * AG-0.2, readiness before Start: the line under the runtime names the CLI
12
+ * it will run (its version and path); when that runtime can't run here,
13
+ * Start is disabled and the line says why and how to fix it (install it,
14
+ * or set agents.runtimes.<id>.bin in the user.yml it names, or run
15
+ * `<cli> agents doctor`). Nothing fails only after Start.
16
+ */
17
+
18
+ function escHtml(s) {
19
+ return String(s ?? "").replace(
20
+ /[&<>"']/g,
21
+ (c) =>
22
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
23
+ c
24
+ ],
25
+ );
26
+ }
27
+
28
+ /** The persona select's options: the id only. */
29
+ export function personaOptions(personas) {
30
+ return (personas ?? [])
31
+ .map((p) => `<option value="${escHtml(p.id)}">${escHtml(p.id)}</option>`)
32
+ .join("");
33
+ }
34
+
35
+ /** The help line under the persona select: the picked persona's role. */
36
+ export function personaHelp(personas, id) {
37
+ return (personas ?? []).find((p) => p.id === id)?.role ?? "";
38
+ }
39
+
40
+ /** A runtime's name for people ("Claude Code"); its id when it has none. */
41
+ export function runtimeName(runtimes, id) {
42
+ const r = (runtimes ?? []).find((x) => x.id === id);
43
+ return r?.label || r?.id || id || "";
44
+ }
45
+
46
+ /**
47
+ * The runtime a persona runs on when the agent names none: the persona's
48
+ * own, else the default runtime.
49
+ */
50
+ export function defaultRuntimeOf(personas, runtimes, personaId) {
51
+ const p = (personas ?? []).find((x) => x.id === personaId);
52
+ if (p?.runtime) return p.runtime;
53
+ return (runtimes ?? []).find((r) => r.default)?.id ?? "";
54
+ }
55
+
56
+ /**
57
+ * The runtime select's options. The first ("" = the persona's) reads
58
+ * "<name> (persona default)"; one that can't run here is disabled and
59
+ * says why.
60
+ */
61
+ export function runtimeOptions(personas, runtimes, personaId, t) {
62
+ const name = runtimeName(
63
+ runtimes,
64
+ defaultRuntimeOf(personas, runtimes, personaId),
65
+ );
66
+ const first = name ? t("ag.runtime.default", { name }) : t("ag.runtime.auto");
67
+ return (
68
+ `<option value="">${escHtml(first)}</option>` +
69
+ (runtimes ?? [])
70
+ .map((r) => {
71
+ const label = runtimeName(runtimes, r.id);
72
+ return r.available
73
+ ? `<option value="${escHtml(r.id)}">${escHtml(
74
+ r.version ? `${label} · ${r.version}` : label,
75
+ )}</option>`
76
+ : `<option value="${escHtml(r.id)}" disabled>${escHtml(
77
+ `${label} — ${r.why || t("ag.unavailable")}`,
78
+ )}</option>`;
79
+ })
80
+ .join("")
81
+ );
82
+ }
83
+
84
+ /** The most searched directories named inline; the rest are counted. */
85
+ const SHOWN_DIRS = 4;
86
+
87
+ /**
88
+ * Whether Start can run, and the line that says so (AG-0.2): for the
89
+ * runtime picked (`runtimeId`, "" = the persona's), { ok, tone, html }.
90
+ * `tone` is "" (ready), "warn" (can't start) or "none" (nothing to say).
91
+ */
92
+ export function readiness(data, personaId, runtimeId, t) {
93
+ const personas = data?.personas ?? [];
94
+ const runtimes = data?.runtimes ?? [];
95
+ const id = runtimeId || defaultRuntimeOf(personas, runtimes, personaId);
96
+ const r = runtimes.find((x) => x.id === id);
97
+ if (!r)
98
+ return {
99
+ ok: false,
100
+ tone: "warn",
101
+ html: escHtml(t("ag.rt.unknown", { id: id || "?" })),
102
+ };
103
+ const name = runtimeName(runtimes, r.id);
104
+ const mono = (x) => `<span class="mono">${escHtml(x)}</span>`;
105
+ if (r.available) {
106
+ if (!r.path) return { ok: true, tone: "none", html: "" };
107
+ return {
108
+ ok: true,
109
+ tone: "",
110
+ html:
111
+ escHtml(
112
+ r.version
113
+ ? t("ag.rt.ready", { name, version: r.version })
114
+ : t("ag.rt.readyNoVer", { name }),
115
+ ) +
116
+ " " +
117
+ mono(r.path),
118
+ };
119
+ }
120
+ const file = data?.userConfig || "config/local/user.yml";
121
+ const fix = t("ag.rt.fix", {
122
+ key: "\u0000k",
123
+ file: "\u0000f",
124
+ cmd: "\u0000c",
125
+ });
126
+ const fixHtml = escHtml(fix)
127
+ .replace("\u0000k", mono(r.setting || `agents.runtimes.${r.id}.bin`))
128
+ .replace("\u0000f", mono(file))
129
+ .replace("\u0000c", mono("__DEN_CLI__ agents doctor"));
130
+ let what;
131
+ if (r.source === "setting" || !r.searched)
132
+ what = escHtml(t("ag.rt.refused", { name, why: r.why || "" }));
133
+ else {
134
+ const dirs = r.searched;
135
+ const shown = dirs.slice(0, SHOWN_DIRS).map(mono).join(", ");
136
+ const more =
137
+ dirs.length > SHOWN_DIRS
138
+ ? " " + escHtml(t("ag.rt.more", { n: dirs.length - SHOWN_DIRS }))
139
+ : "";
140
+ what =
141
+ escHtml(t("ag.rt.notFound", { name })) +
142
+ " " +
143
+ escHtml(t("ag.rt.searched")) +
144
+ " " +
145
+ (shown || escHtml(t("ag.rt.nowhere"))) +
146
+ more +
147
+ ".";
148
+ }
149
+ return { ok: false, tone: "warn", html: `${what} ${fixHtml}` };
150
+ }
151
+
152
+ /**
153
+ * Fill a form's persona and runtime selects, its help line and its
154
+ * readiness line ([data-runtime-ready]), keep them in step when the
155
+ * persona or the runtime changes, and disable its submit button while
156
+ * the runtime can't run. Returns nothing; idempotent per form (the
157
+ * change listeners are bound once).
158
+ */
159
+ export function fillStartForm(form, data, t) {
160
+ const personas = data?.personas ?? [];
161
+ const runtimes = data?.runtimes ?? [];
162
+ const ps = form.querySelector('select[name="persona"]');
163
+ const rs = form.querySelector('select[name="runtime"]');
164
+ const help = form.querySelector("[data-persona-help]");
165
+ const ready = form.querySelector("[data-runtime-ready]");
166
+ const go = form.querySelector('button[type="submit"]');
167
+ ps.innerHTML = personaOptions(personas);
168
+ const check = () => {
169
+ const r = readiness(data, ps.value, rs.value, t);
170
+ if (ready) {
171
+ ready.innerHTML = r.html;
172
+ ready.hidden = !r.html;
173
+ ready.dataset.tone = r.tone;
174
+ }
175
+ if (go) {
176
+ go.disabled = !r.ok;
177
+ go.dataset.blocked = r.ok ? "" : "runtime";
178
+ }
179
+ };
180
+ const sync = () => {
181
+ const keep = rs.value;
182
+ rs.innerHTML = runtimeOptions(personas, runtimes, ps.value, t);
183
+ if ([...rs.options].some((o) => o.value === keep && !o.disabled))
184
+ rs.value = keep;
185
+ if (help) {
186
+ const role = personaHelp(personas, ps.value);
187
+ help.textContent = role;
188
+ help.hidden = !role;
189
+ }
190
+ check();
191
+ };
192
+ ps.onchange = sync;
193
+ rs.onchange = check;
194
+ sync();
195
+ }
196
+
197
+ /** Whether a form's Start is held back by its runtime (readiness). */
198
+ export function blockedByRuntime(form) {
199
+ return (
200
+ form.querySelector('button[type="submit"]')?.dataset.blocked === "runtime"
201
+ );
202
+ }
@@ -15,8 +15,20 @@
15
15
  *
16
16
  * Nothing here keeps a session's state: the list is GET /api/sessions,
17
17
  * asked again every few seconds while the page is shown.
18
+ *
19
+ * Start agent (AG-0.2, LK-1): the lock is checked before the dialog opens
20
+ * (the server, then the vault: checkUnlock). Locked, the dialog asks for
21
+ * the password inline at its top, and Start unlocks, then starts, without
22
+ * closing anything. The runtime line says what will run, or why it
23
+ * can't, before Start (agent-start.js readiness).
18
24
  */
19
- import { requireUnlock } from "/topbar.js";
25
+ import {
26
+ checkUnlock,
27
+ onLockChange,
28
+ requireUnlock,
29
+ unlockWith,
30
+ } from "/topbar.js";
31
+ import { fillStartForm } from "/agent-start.js";
20
32
 
21
33
  const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
22
34
  const esc = (s) =>
@@ -104,6 +116,12 @@ function displayName(r) {
104
116
  return r.kind === "agent" ? (r.persona ?? r.label) : r.label;
105
117
  }
106
118
 
119
+ // A pane's title and its Close: the agent's display name (AG-10.8), else
120
+ // its id, never the persona. There is no display name before AG-10.
121
+ function paneName(r) {
122
+ return r.kind === "agent" ? r.displayName || r.agent || r.label : r.label;
123
+ }
124
+
107
125
  function rowHtml(r) {
108
126
  const open = st.open.includes(r.id);
109
127
  const nm =
@@ -293,7 +311,7 @@ function paintLayout() {
293
311
  function paintBar(p) {
294
312
  const r = rowOf(p.id);
295
313
  const bar = p.el.querySelector(".bar2");
296
- const name = r ? displayName(r) : p.label || p.id;
314
+ const name = r ? paneName(r) : p.label || p.id;
297
315
  const where = r
298
316
  ? [r.workspace, r.runtime].filter(Boolean).join(" · ")
299
317
  : p.workspace || "";
@@ -471,7 +489,7 @@ function closePane(id, { quiet = false } = {}) {
471
489
 
472
490
  async function stopAgent(p, button) {
473
491
  const r = rowOf(p.id);
474
- if (!r?.agent || !requireUnlock()) return;
492
+ if (!r?.agent || !(await requireUnlock())) return;
475
493
  button.disabled = true;
476
494
  try {
477
495
  await api(
@@ -509,28 +527,67 @@ async function openStart() {
509
527
  window.denShell?.errorToast?.(err);
510
528
  return;
511
529
  }
512
- st.root.querySelector("#ag-persona").innerHTML = (data.personas ?? [])
513
- .map(
514
- (p) =>
515
- `<option value="${esc(p.id)}">${esc(p.id)}${p.role ? ` — ${esc(p.role)}` : ""}</option>`,
516
- )
517
- .join("");
518
- st.root.querySelector("#ag-runtime").innerHTML =
519
- `<option value="">${esc(t("ag.runtime.auto"))}</option>` +
520
- (data.runtimes ?? [])
521
- .map(
522
- (r) =>
523
- `<option value="${esc(r.id)}"${r.available ? "" : " disabled"}>${esc(r.id)}${r.available ? "" : ` — ${esc(t("ag.unavailable"))}`}</option>`,
524
- )
525
- .join("");
530
+ fillStartForm(st.root.querySelector("#ag-form"), data, t);
531
+ // LK-1: the lock before the modal. Locked, its password is asked here.
532
+ paintInlineLock(!(await checkUnlock()));
533
+ if (dlg.open) return;
526
534
  if (typeof dlg.showModal === "function") dlg.showModal();
527
535
  else dlg.setAttribute("open", "");
536
+ st.root.querySelector("#ag-lock:not([hidden]) input")?.focus();
537
+ }
538
+
539
+ /** Show (or hide) the dialog's inline password field (LK-1). */
540
+ function paintInlineLock(locked) {
541
+ const box = st.root?.querySelector("#ag-lock");
542
+ if (!box) return;
543
+ box.hidden = !locked;
544
+ const err = box.querySelector("#ag-lock-err");
545
+ if (!locked) {
546
+ box.querySelector("#ag-lock-pw").value = "";
547
+ err.hidden = true;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * The lock, for Start: the dialog's own field when it shows one (unlock,
553
+ * then go on), else the shell's rule (its popover is in the top layer).
554
+ */
555
+ async function unlockForStart(go) {
556
+ const box = st.root.querySelector("#ag-lock");
557
+ if (box.hidden) return requireUnlock();
558
+ if (await checkUnlock()) {
559
+ paintInlineLock(false);
560
+ return true;
561
+ }
562
+ const input = box.querySelector("#ag-lock-pw");
563
+ const err = box.querySelector("#ag-lock-err");
564
+ if (!input.value) {
565
+ err.textContent = t("lock.title");
566
+ err.hidden = false;
567
+ input.focus();
568
+ return false;
569
+ }
570
+ go.disabled = true;
571
+ go.textContent = t("lock.checking");
572
+ const r = await unlockWith(input.value);
573
+ go.disabled = false;
574
+ go.textContent = t("ag.go");
575
+ if (r.ok) {
576
+ paintInlineLock(false);
577
+ return true;
578
+ }
579
+ err.textContent = r.error;
580
+ err.hidden = false;
581
+ input.value = "";
582
+ input.focus();
583
+ return false;
528
584
  }
529
585
 
530
586
  async function submitStart(form) {
531
- if (!requireUnlock()) return;
532
587
  const dlg = st.root.querySelector("#ag-dlg");
533
588
  const go = st.root.querySelector("#ag-go");
589
+ if (go.disabled) return;
590
+ if (!(await unlockForStart(go))) return;
534
591
  const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
535
592
  const persona = form.querySelector("#ag-persona").value;
536
593
  const runtime = form.querySelector("#ag-runtime").value;
@@ -573,6 +630,8 @@ function translateStatic(root) {
573
630
  set("#ag-r-t", "ag.runtime");
574
631
  set("#ag-cancel", "ag.cancel");
575
632
  set("#ag-go", "ag.go");
633
+ set("#ag-lock-t", "lock.pw");
634
+ set("#ag-lock-note", "lock.title");
576
635
  const h = root.querySelector("#ag-dlg-h");
577
636
  if (h) h.textContent = h.textContent.replace(/^\+\s*/, "");
578
637
  }
@@ -608,6 +667,10 @@ export async function mountAgentsPage(root) {
608
667
  root.querySelector("#ag-start").addEventListener("click", () => {
609
668
  void openStart();
610
669
  });
670
+ // Unlocked elsewhere (the chip, another tab): the field goes.
671
+ onLockChange((unlocked) => {
672
+ if (unlocked) paintInlineLock(false);
673
+ });
611
674
  root.querySelector("#ag-cancel").addEventListener("click", () => {
612
675
  root.querySelector("#ag-dlg").close?.();
613
676
  });
@@ -615,6 +678,15 @@ export async function mountAgentsPage(root) {
615
678
  e.preventDefault();
616
679
  void submitStart(e.currentTarget);
617
680
  });
681
+ // Enter starts from any field (a select doesn't submit on its own); Esc
682
+ // cancels (the dialog's own cancel). A button keeps its own Enter.
683
+ root.querySelector("#ag-form").addEventListener("keydown", (e) => {
684
+ if (e.key !== "Enter" || e.isComposing) return;
685
+ if (e.target.closest("button, textarea")) return;
686
+ e.preventDefault();
687
+ if (!st.root.querySelector("#ag-go").disabled)
688
+ e.currentTarget.requestSubmit();
689
+ });
618
690
  // The terminals follow the mode (light, dark) and the theme.
619
691
  new MutationObserver(() => {
620
692
  for (const p of st.panes.values())
@@ -16,6 +16,7 @@
16
16
  * (.den-sec, .den-btn, .den-dot) are reused as they are.
17
17
  */
18
18
  import { requireUnlock } from "/topbar.js";
19
+ import { fillStartForm } from "/agent-start.js";
19
20
 
20
21
  const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
21
22
  const esc = (s) =>
@@ -62,6 +63,8 @@ export function agentsSectionHtml(ws) {
62
63
  <form class="agent-form" id="agent-start-form" hidden>
63
64
  <label>${esc(t("ag.persona"))} <select class="form-sel" name="persona" required></select></label>
64
65
  <label>${esc(t("ag.runtime"))} <select class="form-sel" name="runtime"></select></label>
66
+ <p class="agent-form-help" data-persona-help hidden></p>
67
+ <p class="agent-form-ready" data-runtime-ready role="status" hidden></p>
65
68
  <button type="submit" class="den-btn sm primary" id="agent-start-go">${esc(t("ag.go"))}</button>
66
69
  <button type="button" class="den-btn sm" data-agent-cancel>${esc(t("ag.cancel"))}</button>
67
70
  </form>
@@ -185,22 +188,7 @@ async function refresh(ws) {
185
188
  }
186
189
 
187
190
  function fillForm(form, data) {
188
- const ps = form.querySelector('select[name="persona"]');
189
- ps.innerHTML = (data.personas ?? [])
190
- .map(
191
- (p) =>
192
- `<option value="${esc(p.id)}">${esc(p.id)}${p.role ? ` — ${esc(p.role)}` : ""}</option>`,
193
- )
194
- .join("");
195
- const rs = form.querySelector('select[name="runtime"]');
196
- rs.innerHTML =
197
- `<option value="">${esc(t("ag.runtime.auto"))}</option>` +
198
- (data.runtimes ?? [])
199
- .map(
200
- (r) =>
201
- `<option value="${esc(r.id)}"${r.available ? "" : " disabled"}>${esc(r.id)}${r.available ? "" : ` — ${esc(t("ag.unavailable"))}`}</option>`,
202
- )
203
- .join("");
191
+ fillStartForm(form, data, t);
204
192
  }
205
193
 
206
194
  async function openForm(sec) {
@@ -229,7 +217,7 @@ async function openForm(sec) {
229
217
  }
230
218
 
231
219
  async function act(ws, verb, id, button) {
232
- if (!requireUnlock()) return;
220
+ if (!(await requireUnlock())) return;
233
221
  button.disabled = true;
234
222
  try {
235
223
  await api("POST", `${base(ws)}/${encodeURIComponent(id)}/${verb}`);
@@ -243,7 +231,7 @@ async function act(ws, verb, id, button) {
243
231
  }
244
232
 
245
233
  async function start(ws, form) {
246
- if (!requireUnlock()) return;
234
+ if (!(await requireUnlock())) return;
247
235
  const go = form.querySelector("#agent-start-go");
248
236
  const persona = form.querySelector('select[name="persona"]').value;
249
237
  const runtime = form.querySelector('select[name="runtime"]').value;
@@ -311,8 +311,56 @@
311
311
  }
312
312
  .ag-dlg form {
313
313
  display: grid;
314
+ /* AG-10.0: minmax(0, 1fr), so a long option can't widen the form
315
+ past the dialog and push Cancel and Start out of sight. */
316
+ grid-template-columns: minmax(0, 1fr);
314
317
  gap: 12px;
315
318
  }
319
+ .ag-dlg .help {
320
+ margin: -6px 0 0;
321
+ font: 12.5px/1.45 var(--sans);
322
+ color: var(--muted);
323
+ overflow-wrap: anywhere;
324
+ }
325
+ .ag-dlg .help[hidden],
326
+ .ag-dlg .ready[hidden],
327
+ .ag-dlg .err[hidden],
328
+ .ag-dlg .ag-lock[hidden] {
329
+ display: none;
330
+ }
331
+ /* AG-0.2: the runtime it will run, or why it can't, before Start
332
+ (the mock's .helpl: 12px, faint, 1.4). */
333
+ .ag-dlg .ready {
334
+ margin: -6px 0 0;
335
+ font: 12px/1.4 var(--sans);
336
+ color: var(--faint);
337
+ overflow-wrap: anywhere;
338
+ }
339
+ .ag-dlg .ready[data-tone="warn"] {
340
+ color: var(--warn);
341
+ }
342
+ .ag-dlg .ready .mono {
343
+ font-family: var(--mono);
344
+ }
345
+ /* LK-1: locked, the password is asked here, at the top of the dialog,
346
+ and Start unlocks then starts. */
347
+ .ag-dlg .ag-lock {
348
+ display: grid;
349
+ gap: 6px;
350
+ padding: 10px 12px;
351
+ border: 1px solid
352
+ color-mix(in srgb, var(--warn) 45%, var(--border));
353
+ border-radius: var(--radius-button);
354
+ background: color-mix(in srgb, var(--warn) 8%, var(--raised));
355
+ }
356
+ .ag-dlg .ag-lock .help {
357
+ margin: 0;
358
+ }
359
+ .ag-dlg .err {
360
+ margin: 0;
361
+ font: 12.5px/1.45 var(--sans);
362
+ color: var(--bad);
363
+ }
316
364
  .ag-dlg h2 {
317
365
  margin: 0;
318
366
  font: 600 17px var(--display);
@@ -325,7 +373,10 @@
325
373
  text-transform: uppercase;
326
374
  letter-spacing: 0.07em;
327
375
  }
376
+ .ag-dlg input[type="password"],
328
377
  .ag-dlg select {
378
+ width: 100%;
379
+ min-width: 0;
329
380
  font: 13px var(--sans);
330
381
  text-transform: none;
331
382
  letter-spacing: 0;
@@ -396,6 +447,17 @@
396
447
  <dialog class="ag-dlg" id="ag-dlg" aria-labelledby="ag-dlg-h">
397
448
  <form method="dialog" id="ag-form">
398
449
  <h2 id="ag-dlg-h">Start an agent</h2>
450
+ <div class="ag-lock" id="ag-lock" hidden>
451
+ <label
452
+ ><span id="ag-lock-t">Admin password</span>
453
+ <input
454
+ type="password"
455
+ id="ag-lock-pw"
456
+ autocomplete="current-password"
457
+ /></label>
458
+ <p class="help" id="ag-lock-note"></p>
459
+ <p class="err" id="ag-lock-err" role="alert" hidden></p>
460
+ </div>
399
461
  <label id="ag-ws-l"
400
462
  ><span id="ag-ws-t">Workspace</span>
401
463
  <select name="ws" id="ag-ws"></select
@@ -404,10 +466,18 @@
404
466
  ><span id="ag-p-t">Persona</span>
405
467
  <select name="persona" id="ag-persona" required></select
406
468
  ></label>
469
+ <p class="help" id="ag-p-help" data-persona-help hidden></p>
407
470
  <label
408
471
  ><span id="ag-r-t">Runtime</span>
409
472
  <select name="runtime" id="ag-runtime"></select
410
473
  ></label>
474
+ <p
475
+ class="ready"
476
+ id="ag-r-ready"
477
+ data-runtime-ready
478
+ role="status"
479
+ hidden
480
+ ></p>
411
481
  <div class="btns">
412
482
  <button type="button" class="den-btn" id="ag-cancel">Cancel</button>
413
483
  <button type="submit" class="den-btn primary" id="ag-go">
@@ -330,6 +330,7 @@
330
330
  "ag.persona": "Persona",
331
331
  "ag.runtime": "Runtime",
332
332
  "ag.runtime.auto": "La de la persona",
333
+ "ag.runtime.default": "{name} (la de la persona)",
333
334
  "ag.unavailable": "no disponible",
334
335
  "ag.go": "Iniciar",
335
336
  "ag.starting": "Iniciando…",
@@ -372,5 +373,15 @@
372
373
  "ag.palStart": "Iniciar agente…",
373
374
  "ag.cap":
374
375
  "Hasta 4 paneles: se cerró el de {id} para hacer sitio (sigue en marcha)",
376
+ "ag.rt.ready": "Inicia {name} {version} desde",
377
+ "ag.rt.readyNoVer": "Inicia {name} desde",
378
+ "ag.rt.notFound": "No se encontró {name}.",
379
+ "ag.rt.searched": "Se buscó en:",
380
+ "ag.rt.more": "y {n} más",
381
+ "ag.rt.nowhere": "ningún directorio",
382
+ "ag.rt.refused": "{name} no puede iniciarse: {why}.",
383
+ "ag.rt.fix":
384
+ "Instálalo, o pon su ruta en {key} en {file}, o ejecuta {cmd}.",
385
+ "ag.rt.unknown": "Aquí no hay ningún runtime {id}.",
375
386
  };
376
387
  })((window.denMessages = window.denMessages || {}));
@@ -318,6 +318,7 @@
318
318
  "ag.persona": "ペルソナ",
319
319
  "ag.runtime": "ランタイム",
320
320
  "ag.runtime.auto": "ペルソナの既定",
321
+ "ag.runtime.default": "{name}(ペルソナの既定)",
321
322
  "ag.unavailable": "利用不可",
322
323
  "ag.go": "開始",
323
324
  "ag.starting": "開始中…",
@@ -357,5 +358,15 @@
357
358
  "ag.palStart": "エージェントを開始…",
358
359
  "ag.cap":
359
360
  "ペインは最大 4 つです。{id} のペインを閉じました(実行は続きます)",
361
+ "ag.rt.ready": "{name} {version} を起動:",
362
+ "ag.rt.readyNoVer": "{name} を起動:",
363
+ "ag.rt.notFound": "{name} が見つかりません。",
364
+ "ag.rt.searched": "検索した場所:",
365
+ "ag.rt.more": "ほか {n} 件",
366
+ "ag.rt.nowhere": "なし",
367
+ "ag.rt.refused": "{name} を起動できません: {why}。",
368
+ "ag.rt.fix":
369
+ "インストールするか、{file} の {key} にパスを設定するか、{cmd} を実行してください。",
370
+ "ag.rt.unknown": "ランタイム {id} はここにありません。",
360
371
  };
361
372
  })((window.denMessages = window.denMessages || {}));