@monster-orange/den 0.4.18 → 0.4.20

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 (53) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.md +14 -13
  3. package/docs/distributions.md +148 -30
  4. package/docs/reference.md +1 -1
  5. package/package.json +2 -1
  6. package/skills/install-den/SKILL.md +35 -75
  7. package/src/shared/agents/den-claude.ts +40 -6
  8. package/src/shared/agents/guardrails/stop-hook.ts +30 -4
  9. package/src/shared/agents/guardrails.ts +40 -4
  10. package/src/shared/agents/handback.ts +22 -4
  11. package/src/shared/agents/personas.ts +32 -5
  12. package/src/shared/agents/runtime.ts +8 -0
  13. package/src/shared/agents/server-agent.ts +57 -0
  14. package/src/shared/agents/server-pack/server.md +45 -0
  15. package/src/shared/environment-manager/workspaces.ts +4 -0
  16. package/src/shared/secrets/index.ts +71 -3
  17. package/src/shared/secrets/onepassword.ts +87 -14
  18. package/src/shared/secrets/op-token.ts +270 -0
  19. package/src/shared/secrets/secret-env.ts +19 -6
  20. package/src/shared/secrets/strategies.ts +6 -0
  21. package/src/shared/secrets/types.ts +8 -0
  22. package/src/ui/cli/agent-ops.ts +41 -6
  23. package/src/ui/cli/create.ts +2 -2
  24. package/src/ui/cli/doctor-command.ts +54 -2
  25. package/src/ui/cli/index.ts +1 -1
  26. package/src/ui/cli/repo-command.ts +8 -1
  27. package/src/ui/cli/repo-detect.ts +58 -0
  28. package/src/ui/cli/server-agent-ops.ts +411 -0
  29. package/src/ui/mcp/server.ts +29 -12
  30. package/src/ui/mcp/tools.ts +4 -2
  31. package/src/ui/server/agent-routes.ts +1 -1
  32. package/src/ui/server/held-actions.ts +35 -7
  33. package/src/ui/server/index.ts +25 -6
  34. package/src/ui/server/public/agents-page.js +10 -1
  35. package/src/ui/server/public/error-page.js +39 -1
  36. package/src/ui/server/public/graph.css +21 -0
  37. package/src/ui/server/public/graph.js +49 -6
  38. package/src/ui/server/public/header.js +29 -7
  39. package/src/ui/server/public/i18n/es.js +38 -0
  40. package/src/ui/server/public/i18n/ja.js +40 -0
  41. package/src/ui/server/public/local-dev.html +22 -1
  42. package/src/ui/server/public/persona-ui.css +6 -0
  43. package/src/ui/server/public/server-agent-page.js +415 -0
  44. package/src/ui/server/public/server-agent.html +222 -0
  45. package/src/ui/server/public/settings-registry.js +22 -0
  46. package/src/ui/server/public/shell.js +67 -6
  47. package/src/ui/server/public/svc-panel.js +27 -14
  48. package/src/ui/server/public/topbar.js +13 -1
  49. package/src/ui/server/route-classes.ts +21 -0
  50. package/src/ui/server/server-agent-routes.ts +176 -0
  51. package/src/ui/server/session-routes.ts +12 -1
  52. package/src/ui/server/ui-assets.ts +2 -0
  53. package/src/ui/server/workspace-routes.ts +3 -0
@@ -50,6 +50,7 @@ import { readCatalog } from "../../shared/environment-manager/catalog.js";
50
50
  import {
51
51
  getActiveWorkspace,
52
52
  getActiveWorkspaces,
53
+ activateWorkspace,
53
54
  createWorkspace,
54
55
  getFocusedWorkspaceName,
55
56
  getWorkspaceByName,
@@ -172,6 +173,7 @@ import {
172
173
  } from "../../shared/auth/sudo-vault.js";
173
174
  import {
174
175
  notConfiguredMessage,
176
+ opTokenSourceSetting,
175
177
  secretsConfigWarnings,
176
178
  } from "../../shared/secrets/index.js";
177
179
  import { DEN_BANNER_LINES, DEN_SUBTITLE } from "../../shared/banner.js";
@@ -858,6 +860,8 @@ import { heldActions } from "./held-actions.js";
858
860
  import type { ApprovalQueue } from "./approvals.js";
859
861
  import { processPathRules } from "../cli/onboard-reads.js";
860
862
  import { handleAgentRoutes, startAgentUpkeep } from "./agent-routes.js";
863
+ import { handleServerAgentRoutes } from "./server-agent-routes.js";
864
+ import { freshDen } from "../cli/server-agent-ops.js";
861
865
  import {
862
866
  lastRunsByRepo,
863
867
  recordRunEnd,
@@ -1768,6 +1772,8 @@ function approvalDeps(): {
1768
1772
  validate: (m) => validateManifest(m),
1769
1773
  workspaceCreateRefusal,
1770
1774
  createWorkspace: (name) => createWorkspace(name, { services: {} }),
1775
+ activateWorkspace: (name) => activateWorkspace(name),
1776
+ workspaceActive: (name) => isWorkspaceActive(name),
1771
1777
  workspaceExists: (name) =>
1772
1778
  Object.prototype.hasOwnProperty.call(
1773
1779
  readWorkspacesFile().workspaces ?? {},
@@ -1775,12 +1781,8 @@ function approvalDeps(): {
1775
1781
  ),
1776
1782
  defaultWorkspace: () => getFocusedWorkspaceName() ?? "default",
1777
1783
  // The exec route's checks for a local `up` (R13 placement, HITL).
1784
+ // An inactive workspace is fine: the Allow activates it (held-actions.ts).
1778
1785
  serviceTarget: (service, ws) => {
1779
- if (ws && !isWorkspaceActive(ws))
1780
- return {
1781
- ok: false,
1782
- error: `Workspace "${ws}" is not active — activate it first`,
1783
- };
1784
1786
  const resolved = resolveService(service, ws);
1785
1787
  if (!resolved)
1786
1788
  return {
@@ -2140,9 +2142,18 @@ async function handleRequest(
2140
2142
  res.end();
2141
2143
  return;
2142
2144
  }
2145
+ // ON-6: a fresh Den's first page is its own agent (no redirect: a
2146
+ // setup link's #ak= fragment stays on this address).
2147
+ if (pathname === "/" && (await freshDen()))
2148
+ return serveFile(res, path.join(PUBLIC_DIR, "server-agent.html"));
2143
2149
  return serveFile(res, path.join(PUBLIC_DIR, "local-dev.html"));
2144
2150
  }
2145
2151
 
2152
+ // The server agent's page (ON-6): this Den's own agent, in a terminal.
2153
+ if (pathname === "/server-agent" || pathname === "/server-agent.html") {
2154
+ return serveFile(res, path.join(PUBLIC_DIR, "server-agent.html"));
2155
+ }
2156
+
2146
2157
  // The Graph page (R22): Local Dev's page in its full-width graph view.
2147
2158
  if (pathname === "/graph" || pathname === "/graph.html") {
2148
2159
  if (!isLocalEnabled()) {
@@ -4284,6 +4295,9 @@ async function handleRequest(
4284
4295
  uptimeMs: Date.now() - SERVER_STARTED_AT,
4285
4296
  stale,
4286
4297
  scanned,
4298
+ // ON-6: no repo in any workspace yet (the server agent page's own
4299
+ // `fresh`): the shell skips What's new on a brand-new Den.
4300
+ fresh: await freshDen(),
4287
4301
  });
4288
4302
  }
4289
4303
 
@@ -4587,6 +4601,8 @@ async function handleRequest(
4587
4601
  return;
4588
4602
  }
4589
4603
 
4604
+ // ── The server agent (ON-6, server-agent-routes.ts) ──────────────────────
4605
+ if (await handleServerAgentRoutes(req, res, pathname)) return;
4590
4606
  // ── Agents on the Workspaces page (AG-4a, agent-routes.ts) ───────────────
4591
4607
  if (await handleAgentRoutes(req, res, pathname)) return;
4592
4608
  // ── The Agents tab's list (AG-4a-term, session-routes.ts) ────────────────
@@ -4845,7 +4861,8 @@ async function handleRequest(
4845
4861
  // ── API: user settings ────────────────────────────────────────────────────
4846
4862
  // GET /api/settings → { editor, editors, terminal, terminals, editor_overrides, languages,
4847
4863
  // log_window_lines, graph, appearance, shell, distribution,
4848
- // user_web_port, local_dev, cli_tabs, language, ui_languages }
4864
+ // user_web_port, local_dev, cli_tabs, language, ui_languages,
4865
+ // op_token_source }
4849
4866
  // POST /api/settings body: { editor?, terminal?, editor_overrides?, graph?, appearance?, shell?,
4850
4867
  // language? }
4851
4868
  // graph: { direction, group, density, panelWidth } (see graph-prefs.ts)
@@ -4914,6 +4931,8 @@ async function handleRequest(
4914
4931
  // what it is when unset (the short hostname).
4915
4932
  machine_name: machineNameSetting(user),
4916
4933
  machine_name_default: machineLabel(null, os.hostname()),
4934
+ // Where the 1Password token comes from (the source, never the token).
4935
+ op_token_source: opTokenSourceSetting(),
4917
4936
  });
4918
4937
  }
4919
4938
  if (req.method === "POST") {
@@ -125,6 +125,8 @@ const esc = (s) =>
125
125
  const api = (method, url, body) => window.denShell.api(method, url, body);
126
126
  const CLI = "__DEN_CLI__";
127
127
  const TITLE = "__DEN_TITLE__";
128
+ /** ON-6: the server agent's scope (no workspace), shared/agents/server-agent.ts. */
129
+ const SERVER_WS = "den-server";
128
130
  /** How long Remove's Undo stays offered (AG-14.1b). */
129
131
  export const UNDO_MS = 10_000;
130
132
 
@@ -339,6 +341,9 @@ function wsHref(ws) {
339
341
 
340
342
  /** Workspace `ws` as a link to it on Workspaces. */
341
343
  function wsLinkHtml(ws) {
344
+ // ON-6: the server agent belongs to no workspace: its group is "Server".
345
+ if (ws === SERVER_WS)
346
+ return `<span class="wslink" data-server>${esc(t("ag.server"))}</span>`;
342
347
  return `<a class="wslink" href="${esc(wsHref(ws))}" title="${esc(t("ag.goWs", { ws }))}">${esc(ws)}</a>`;
343
348
  }
344
349
 
@@ -455,7 +460,10 @@ function paintList() {
455
460
  el.innerHTML = notes.join("");
456
461
  return;
457
462
  }
458
- const shown = viewWorkspaces();
463
+ // ON-6: the server agent's group, "Server", first, whatever the view.
464
+ const shown = st.data.sessions.some((s) => s.server)
465
+ ? [SERVER_WS, ...viewWorkspaces()]
466
+ : viewWorkspaces();
459
467
  // ?ws= names a workspace that isn't active: say so, and offer the switch.
460
468
  const asked = st.data.requested;
461
469
  if (asked && !asked.active)
@@ -515,6 +523,7 @@ function paintList() {
515
523
 
516
524
  function visibleRows() {
517
525
  const shown = new Set(viewWorkspaces());
526
+ shown.add(SERVER_WS);
518
527
  return (st.data?.sessions ?? []).filter((s) => shown.has(s.workspace));
519
528
  }
520
529
 
@@ -171,6 +171,34 @@ function cmdLine(id, cmd, t, extra = "") {
171
171
  );
172
172
  }
173
173
 
174
+ /** Whether this browser runs on Windows (no ssh:// app by default). */
175
+ function onWindows() {
176
+ const nav = globalThis.navigator;
177
+ const platform = nav?.userAgentData?.platform || nav?.platform || "";
178
+ return /^win/i.test(platform) || /Windows/i.test(nav?.userAgent || "");
179
+ }
180
+
181
+ /** How long a click on Open Terminal waits for an app to take the page's focus. */
182
+ const NOTHING_OPENED_MS = 2000;
183
+
184
+ /**
185
+ * SEC-2a-UX.2: after a click on Open Terminal, if this page still has
186
+ * focus 2 s later, no app took the ssh:// link; say so under the button.
187
+ */
188
+ function wireNothingOpened(root) {
189
+ const term = root.querySelector("#den-pair-term");
190
+ const note = root.querySelector("#den-pair-nothing");
191
+ if (!term || !note) return;
192
+ let timer = null;
193
+ term.addEventListener("click", () => {
194
+ clearTimeout(timer);
195
+ timer = setTimeout(() => {
196
+ if (!document.hidden && document.hasFocus() && note.isConnected)
197
+ note.hidden = false;
198
+ }, window.__denNothingOpenedMs || NOTHING_OPENED_MS);
199
+ });
200
+ }
201
+
174
202
  /** Wire every copy button under `root` (each once). */
175
203
  function wireCopy(root, t) {
176
204
  root.querySelectorAll("button[data-copy-for]").forEach((b) => {
@@ -445,9 +473,12 @@ function wirePair(root, t, s) {
445
473
  `<div class="acts" style="margin-top:0"><button type="button" class="btn" id="den-pair-new">${esc(t("err.retry"))}</button></div>`;
446
474
  box.querySelector("#den-pair-new").addEventListener("click", begin);
447
475
  };
476
+ // SEC-2a-UX.2: Windows has no ssh:// app by default, so the link would
477
+ // do nothing. There the command itself is the step.
478
+ const windows = onWindows();
448
479
  const show = (b) => {
449
480
  const term =
450
- s.ssh?.name && target
481
+ s.ssh?.name && target && !windows
451
482
  ? `<a class="btn" id="den-pair-term" href="ssh://${esc(login)}">${esc(t("err.signin.openTerminal"))}</a>`
452
483
  : "";
453
484
  box.innerHTML =
@@ -459,8 +490,14 @@ function wirePair(root, t, s) {
459
490
  `<p class="den-pair-wait" id="den-pair-status">${esc(t("err.signin.waiting"))} <span class="mono" id="den-pair-left"></span></p></li>` +
460
491
  `</ol>` +
461
492
  `<p>${withHost(t, "err.signin.noTerminal", { host })}</p>` +
493
+ (windows && b.ssh
494
+ ? `<p class="den-pair-win" id="den-pair-win">${esc(t("err.signin.windowsSteps"))}</p>`
495
+ : "") +
462
496
  cmdLine("den-pair-ssh", b.ssh, t, term) +
463
497
  (term
498
+ ? `<p class="den-pair-nothing" id="den-pair-nothing" role="status" hidden>${esc(t("err.signin.nothingOpened"))}</p>`
499
+ : "") +
500
+ (term || (windows && b.ssh && target)
464
501
  ? `<p class="den-pair-note" id="den-pair-note">${
465
502
  user
466
503
  ? withHost(t, "err.signin.sshUserNote", { host, user })
@@ -468,6 +505,7 @@ function wirePair(root, t, s) {
468
505
  }</p>`
469
506
  : "");
470
507
  wireCopy(box, t);
508
+ wireNothingOpened(box);
471
509
  };
472
510
  function begin() {
473
511
  stop();
@@ -184,6 +184,27 @@
184
184
  vertical-align: middle;
185
185
  margin-right: 6px;
186
186
  }
187
+ /* Open in Graph for a service outside the View: one line, Show <ws>. */
188
+ .dg-out {
189
+ position: absolute;
190
+ top: 12px;
191
+ left: 12px;
192
+ right: 12px;
193
+ z-index: 960;
194
+ display: flex;
195
+ flex-wrap: wrap;
196
+ align-items: center;
197
+ justify-content: center;
198
+ gap: 8px 12px;
199
+ width: fit-content;
200
+ margin: 0 auto;
201
+ padding: 8px 12px;
202
+ border: 1px solid var(--border);
203
+ border-radius: 8px;
204
+ background: var(--surface);
205
+ color: var(--text);
206
+ font-size: 13px;
207
+ }
187
208
  .dg-msg {
188
209
  padding: 40px;
189
210
  color: var(--muted);
@@ -262,20 +262,63 @@ export function mountGraph(el, data, pageHooks) {
262
262
 
263
263
  /** 0.4.18: the panel's Open in Graph (#focus=<repo> or <repo>/<service>), once. */
264
264
  let hashFocused = false;
265
- function focusFromHash() {
266
- if (hashFocused || !location.hash.startsWith("#focus=")) return;
267
- let id = "";
265
+ /** The id #focus= names, or "" (none, or not decodable). */
266
+ function hashFocusId() {
267
+ if (!location.hash.startsWith("#focus=")) return "";
268
268
  try {
269
- id = decodeURIComponent(location.hash.slice(7));
269
+ return decodeURIComponent(location.hash.slice(7));
270
270
  } catch {
271
- return;
271
+ return "";
272
272
  }
273
+ }
274
+ function focusFromHash() {
275
+ if (hashFocused) return;
276
+ const id = hashFocusId();
277
+ if (!id) return;
278
+ host.querySelector(".dg-out")?.remove();
273
279
  if (M.byId.has(id)) focusOn({ id });
274
280
  else if (M.byRepo.has(id)) focusOn({ repo: id, viaRepo: true });
275
- else return;
281
+ else return outsideNote(id);
276
282
  hashFocused = true;
277
283
  }
278
284
 
285
+ /**
286
+ * #focus= names a service in a workspace outside the View: one line that
287
+ * says so, and Show <workspace>, which switches the View the way the top
288
+ * bar does (the page's hook); the reloaded graph then focuses the card.
289
+ * The View never switches by itself. `into` is where the line goes: the
290
+ * graph's viewport, or the empty state's box (graphEmptyNote).
291
+ */
292
+ function outsideNote(id, into = $(".dg-vp"), h = hooks) {
293
+ const ws = h.outsideView?.(id);
294
+ if (!ws) return;
295
+ const t = (k, v) => window.denShell?.t?.(k, v) ?? k;
296
+ const note = document.createElement("div");
297
+ note.className = "dg-out";
298
+ note.setAttribute("role", "status");
299
+ note.innerHTML =
300
+ `<span>${esc(t("graph.outside", { s: id, ws }))}</span>` +
301
+ `<button type="button" class="den-btn sm" data-show-ws>${esc(t("graph.showWs", { ws }))}</button>`;
302
+ note.querySelector("[data-show-ws]").addEventListener("click", (e) => {
303
+ e.currentTarget.disabled = true;
304
+ void h.showWorkspace?.(ws);
305
+ });
306
+ into?.append(note);
307
+ }
308
+
309
+ /**
310
+ * 0.4.19: the View's workspace has no services, so no graph is drawn (the
311
+ * page's "No services in workspace"), and #focus= names a service in
312
+ * another workspace (Open in Graph right after onboarding): the same line
313
+ * and Show <workspace> as over a drawn graph, in `el`.
314
+ */
315
+ export function graphEmptyNote(el, pageHooks) {
316
+ const id = hashFocusId();
317
+ if (!el || !id) return;
318
+ el.querySelector(".dg-out")?.remove();
319
+ outsideNote(id, el, pageHooks || {});
320
+ }
321
+
279
322
  /** A /api/health update: refresh dots, health lines and the open panel in place. */
280
323
  export function graphHealth(results) {
281
324
  if (!M || !host?.isConnected) return;
@@ -650,15 +650,15 @@ export async function initChangelog() {
650
650
  }
651
651
 
652
652
  const latest = _changelogEntries[0];
653
+ let info = null;
654
+ try {
655
+ info = await fetch("/api/server-info").then((r) => r.json());
656
+ } catch {
657
+ /* fall back to the changelog's newest version */
658
+ }
653
659
  const pill = document.getElementById("version-pill");
654
660
  if (pill) {
655
- let running = null;
656
- try {
657
- running = (await fetch("/api/server-info").then((r) => r.json())).version;
658
- } catch {
659
- /* fall back to the changelog's newest version */
660
- }
661
- const label = versionLabel(running, _changelogEntries);
661
+ const label = versionLabel(info?.version ?? null, _changelogEntries);
662
662
  if (label) pill.querySelector(".v-txt").textContent = label;
663
663
  }
664
664
 
@@ -667,6 +667,16 @@ export async function initChangelog() {
667
667
  lastSeen = localStorage.getItem(LS_CHANGELOG_SEEN);
668
668
  } catch {}
669
669
 
670
+ // ON-6: a brand-new Den (no repo yet) opens on its agent and the
671
+ // approvals: no release notes over them. This version counts as seen.
672
+ if (info?.fresh === true) {
673
+ if (latest?.version)
674
+ try {
675
+ localStorage.setItem(LS_CHANGELOG_SEEN, latest.version);
676
+ } catch {}
677
+ return;
678
+ }
679
+
670
680
  if (latest?.version && lastSeen !== latest.version) {
671
681
  const pip = document.getElementById("v-pip");
672
682
  if (pip) pip.style.display = "block";
@@ -902,6 +912,18 @@ async function _switchScope(value, focus, inView) {
902
912
  location.reload();
903
913
  }
904
914
 
915
+ /**
916
+ * Switch the View to one workspace, as the top bar's Workspace menu does
917
+ * (the Graph page's Show <workspace>): the CLI default, then the view,
918
+ * then a reload (the URL's hash stays).
919
+ */
920
+ export async function switchView(name) {
921
+ const scope = await fetch("/api/scope")
922
+ .then((r) => (r.ok ? r.json() : null))
923
+ .catch(() => null);
924
+ await _switchScope(name, scope?.focus ?? null, false);
925
+ }
926
+
905
927
  /**
906
928
  * Make `name` the CLI default: Local Dev's focus action (POST
907
929
  * /api/workspaces `focus`, which writes workspaces.yml and retargets the
@@ -86,6 +86,8 @@
86
86
  "svp.startHere": "Iniciar agente aquí",
87
87
  "svp.openSvc": "Abrir en Servicios",
88
88
  "svp.openGraph": "Abrir en Grafo",
89
+ "graph.outside": "{s} está en {ws}, fuera de esta vista.",
90
+ "graph.showWs": "Mostrar {ws}",
89
91
  "svp.close": "Cerrar",
90
92
  "ag.new.folder": "Carpeta (opcional)",
91
93
  "ag.new.folderHelp":
@@ -313,6 +315,10 @@
313
315
  "err.signin.step2":
314
316
  "Esta página inicia sesión sola una vez aprobada. El código caduca en 10 minutos.",
315
317
  "err.signin.waiting": "Esperando la aprobación…",
318
+ "err.signin.windowsSteps":
319
+ "Abre Windows Terminal o PowerShell, pega esto y pulsa Intro.",
320
+ "err.signin.nothingOpened":
321
+ "¿No se abrió nada? Copia el comando de abajo y pégalo en tu app de terminal.",
316
322
  "err.signin.noTerminal":
317
323
  "¿No tienes una terminal en {host}? Desde este ordenador, ejecuta:",
318
324
  "err.signin.openTerminal": "Abrir Terminal",
@@ -396,6 +402,7 @@
396
402
  "set.cli.web_port": "Tu puerto web",
397
403
  "set.cli.signin": "Inicio de sesión del navegador",
398
404
  "set.cli.sudo": "Contraseña de administrador",
405
+ "set.cli.op_token": "Origen del token de 1Password",
399
406
  "set.access.owner": "Propietario",
400
407
  "set.access.devices": "Dispositivos vinculados",
401
408
  "set.access.waiting": "Dispositivos en espera",
@@ -580,6 +587,8 @@
580
587
  "ap.do.repo_add.new":
581
588
  'Añadir el repositorio de {path} a un espacio nuevo "{workspace}"',
582
589
  "ap.do.service_try": "Iniciar {service}",
590
+ "ap.do.service_try.activates":
591
+ "Iniciar {service} (esto activa {workspace})",
583
592
  "ap.says": "El agente dice:",
584
593
  "ap.review": "Revisar",
585
594
  "ap.hide": "Ocultar detalles",
@@ -724,6 +733,35 @@
724
733
  "ag.ws": "Espacio de trabajo",
725
734
  "ag.pal": "Agentes › {ws} / {name}",
726
735
  "ag.palStart": "Iniciar agente…",
736
+ // ON-6: the server agent (server-agent-page.js, the rail, ⌘K).
737
+ "n.serverAgent": "Agente de __DEN_TITLE__",
738
+ "ag.server": "Servidor",
739
+ "pal.askServer": "Preguntar a __DEN_TITLE__",
740
+ "sa.sub.fresh":
741
+ "Vamos a configurar __DEN_TITLE__. Solo lee hasta que permitas un cambio.",
742
+ "sa.sub":
743
+ "Para todo lo del servidor: añadir un repo, arreglar un servicio que falla, cambiar un ajuste o explicar qué está en marcha.",
744
+ "sa.trust": "Claude pregunta si confías en esta carpeta. Elige Sí.",
745
+ "sa.model": "Modelo",
746
+ "sa.login":
747
+ "Si Claude te pide iniciar sesión, hazlo aquí. Después {name} empieza.",
748
+ "sa.readsOnly":
749
+ "Solo actúa con las herramientas de __DEN_TITLE__. Cada cambio te lo pregunta antes, en este navegador.",
750
+ "sa.starting": "Iniciando el agente…",
751
+ "sa.idle": "{name} no está en marcha.",
752
+ "sa.start": "Iniciar",
753
+ "sa.failed": "No se inició: {why}. {cli} agents doctor da más detalles.",
754
+ "sa.unread":
755
+ "No se pudo leer el estado del agente. ¿Está en marcha el servidor web?",
756
+ "sa.none.h": "Aún no hay ningún runtime para el agente",
757
+ "sa.none":
758
+ "El agente se ejecuta en una CLI de agente del catálogo de runtimes. Instala una, inicia sesión en ella una vez y vuelve a cargar esta página.",
759
+ "sa.noTools": "aún no puede usar las herramientas de __DEN_TITLE__",
760
+ "sa.manual.h": "O configúralo tú",
761
+ "sa.manual": "Cada paso del agente también es un comando. En un terminal:",
762
+ "sa.runsOn": "Se ejecuta en",
763
+ "sa.openAgents": "Abrir en Agentes",
764
+ "sa.saved": "Guardado. El agente lo usa desde su próximo inicio.",
727
765
  "ag.cap":
728
766
  "Hasta 4 paneles: se cerró el de {id} para hacer sitio (sigue en marcha)",
729
767
  "ag.layout": "Disposición",
@@ -83,6 +83,8 @@
83
83
  "svp.startHere": "ここでエージェントを開始",
84
84
  "svp.openSvc": "サービスで開く",
85
85
  "svp.openGraph": "グラフで開く",
86
+ "graph.outside": "{s} は {ws} にあり、この表示の範囲外です。",
87
+ "graph.showWs": "{ws} を表示",
86
88
  "svp.close": "閉じる",
87
89
  "ag.new.folder": "フォルダー(任意)",
88
90
  "ag.new.folderHelp":
@@ -302,6 +304,10 @@
302
304
  "err.signin.step2":
303
305
  "承認されると、このページは自動でサインインします。コードの有効期限は 10 分です。",
304
306
  "err.signin.waiting": "承認を待っています…",
307
+ "err.signin.windowsSteps":
308
+ "Windows ターミナルか PowerShell を開き、これを貼り付けて Enter キーを押します。",
309
+ "err.signin.nothingOpened":
310
+ "何も開きませんでしたか?下のコマンドをコピーして、ターミナルアプリに貼り付けてください。",
305
311
  "err.signin.noTerminal":
306
312
  "{host} のターミナルがありませんか?このコンピューターで次を実行します:",
307
313
  "err.signin.openTerminal": "ターミナルを開く",
@@ -385,6 +391,7 @@
385
391
  "set.cli.web_port": "あなたの web ポート",
386
392
  "set.cli.signin": "ブラウザのサインイン",
387
393
  "set.cli.sudo": "管理者パスワード",
394
+ "set.cli.op_token": "1Password トークンの取得元",
388
395
  "set.access.owner": "オーナー",
389
396
  "set.access.devices": "リンク済みデバイス",
390
397
  "set.access.waiting": "承認待ちのデバイス",
@@ -566,6 +573,8 @@
566
573
  "ap.do.repo_add.new":
567
574
  "{path} のリポジトリを新しいワークスペース「{workspace}」に追加",
568
575
  "ap.do.service_try": "{service} を起動",
576
+ "ap.do.service_try.activates":
577
+ "{service} を起動 ({workspace} を有効にします)",
569
578
  "ap.says": "エージェントのコメント:",
570
579
  "ap.review": "確認",
571
580
  "ap.hide": "詳細を隠す",
@@ -707,6 +716,37 @@
707
716
  "ag.ws": "ワークスペース",
708
717
  "ag.pal": "エージェント › {ws} / {name}",
709
718
  "ag.palStart": "エージェントを開始…",
719
+ // ON-6: the server agent (server-agent-page.js, the rail, ⌘K).
720
+ "n.serverAgent": "__DEN_TITLE__ エージェント",
721
+ "ag.server": "サーバー",
722
+ "pal.askServer": "__DEN_TITLE__ に聞く",
723
+ "sa.sub.fresh":
724
+ "__DEN_TITLE__ をセットアップしましょう。変更を許可するまで、読み取りだけを行います。",
725
+ "sa.sub":
726
+ "サーバーのことなら何でも:リポジトリの追加、失敗しているサービスの修正、設定の変更、動いているものの説明。",
727
+ "sa.trust":
728
+ "Claude がこのフォルダーを信頼するか尋ねます。「はい」を選んでください。",
729
+ "sa.model": "モデル",
730
+ "sa.login":
731
+ "Claude からログインを求められたら、ここでログインしてください。そのあと {name} が始まります。",
732
+ "sa.readsOnly":
733
+ "__DEN_TITLE__ 自身のツールだけで動きます。変更はそのたびに、このブラウザーで先に確認します。",
734
+ "sa.starting": "エージェントを起動しています…",
735
+ "sa.idle": "{name} は動いていません。",
736
+ "sa.start": "開始",
737
+ "sa.failed":
738
+ "起動しませんでした:{why}。詳しくは {cli} agents doctor で確認できます。",
739
+ "sa.unread":
740
+ "エージェントの状態を読めませんでした。Web サーバーは動いていますか?",
741
+ "sa.none.h": "エージェントを動かせるランタイムがまだありません",
742
+ "sa.none":
743
+ "エージェントはランタイムカタログのエージェント CLI で動きます。1 つインストールして一度ログインし、このページを再読み込みしてください。",
744
+ "sa.noTools": "まだ __DEN_TITLE__ のツールを使えません",
745
+ "sa.manual.h": "自分でセットアップする",
746
+ "sa.manual": "エージェントの各手順はコマンドでもできます。ターミナルで:",
747
+ "sa.runsOn": "実行するランタイム",
748
+ "sa.openAgents": "エージェントで開く",
749
+ "sa.saved": "保存しました。次回の起動から使われます。",
710
750
  "ag.cap":
711
751
  "ペインは最大 4 つです。{id} のペインを閉じました(実行は続きます)",
712
752
  "ag.layout": "レイアウト",
@@ -1544,6 +1544,7 @@
1544
1544
  copyToClipboard,
1545
1545
  computePrimaryByRepo,
1546
1546
  setServiceRunner,
1547
+ switchView,
1547
1548
  sudoRun,
1548
1549
  takePendingAction,
1549
1550
  problemCards,
@@ -1552,7 +1553,12 @@
1552
1553
  } from "/header.js";
1553
1554
  import { checkLine } from "/topbar.js";
1554
1555
  import { agentsSectionHtml, mountAgents } from "/agents-section.js";
1555
- import { mountGraph, graphHealth, graphMaximized } from "/graph.js";
1556
+ import {
1557
+ mountGraph,
1558
+ graphEmptyNote,
1559
+ graphHealth,
1560
+ graphMaximized,
1561
+ } from "/graph.js";
1556
1562
  import {
1557
1563
  mergeWorkspaceViews,
1558
1564
  ownerOfRepos,
@@ -4653,6 +4659,19 @@
4653
4659
  });
4654
4660
 
4655
4661
  const GRAPH_HOOKS = {
4662
+ /**
4663
+ * Open in Graph for a service this graph doesn't draw: the
4664
+ * workspace it is in, outside the View (#focus=, graph.js), or
4665
+ * null when it is in no other workspace.
4666
+ */
4667
+ outsideView: (id) => {
4668
+ const shown = GRAPH_ALL
4669
+ ? GRAPH_ALL.names
4670
+ : [document.getElementById("content")?.dataset.ws || activeWs];
4671
+ return wsOfAddress(id).find((w) => !shown.includes(w)) ?? null;
4672
+ },
4673
+ /** Its Show <workspace>: the top bar's switch (header.js). */
4674
+ showWorkspace: (ws) => switchView(ws),
4656
4675
  maximize: {
4657
4676
  isOn: () => graphMax(),
4658
4677
  toggle: () => setGraphMax(!graphMax()),
@@ -4701,6 +4720,8 @@
4701
4720
  if (!services || services.length === 0) {
4702
4721
  wrap.innerHTML =
4703
4722
  '<div class="graph-empty">No services in workspace</div>';
4723
+ // Open in Graph for a service in another workspace: say where.
4724
+ graphEmptyNote(wrap, GRAPH_HOOKS);
4704
4725
  return;
4705
4726
  }
4706
4727
  mountGraph(
@@ -345,6 +345,12 @@ dialog.pe-dlg::backdrop {
345
345
  color: var(--faint);
346
346
  font-weight: 400;
347
347
  }
348
+ /* On the chosen row's tint, faint text falls under 4.5:1 (light): the
349
+ role and a plain tag read in muted there. */
350
+ .pr[aria-checked="true"] .pn .role,
351
+ .pr[aria-checked="true"] .tagx:not(.warn, .src-local, .src-distribution, .src-hub) {
352
+ color: var(--muted);
353
+ }
348
354
  .pr .pd {
349
355
  grid-column: 2 / 4;
350
356
  font-size: 12.5px;