@monster-orange/den 0.4.17 → 0.4.19
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/CHANGELOG.md +177 -0
- package/README.md +14 -13
- package/docs/distributions.md +138 -25
- package/docs/reference.md +1 -1
- package/package.json +2 -1
- package/skills/install-den/SKILL.md +35 -75
- package/src/shared/agents/den-claude.ts +40 -6
- package/src/shared/agents/guardrails/stop-hook.ts +30 -4
- package/src/shared/agents/guardrails.ts +40 -4
- package/src/shared/agents/handback.ts +22 -4
- package/src/shared/agents/personas.ts +32 -5
- package/src/shared/agents/runtime.ts +8 -0
- package/src/shared/agents/server-agent.ts +57 -0
- package/src/shared/agents/server-pack/server.md +45 -0
- package/src/shared/environment-manager/workspaces.ts +4 -0
- package/src/ui/cli/agent-ops.ts +84 -6
- package/src/ui/cli/create.ts +2 -2
- package/src/ui/cli/index.ts +1 -1
- package/src/ui/cli/repo-command.ts +8 -1
- package/src/ui/cli/repo-detect.ts +58 -0
- package/src/ui/cli/server-agent-ops.ts +411 -0
- package/src/ui/mcp/server.ts +29 -12
- package/src/ui/mcp/tools.ts +4 -2
- package/src/ui/server/agent-routes.ts +10 -1
- package/src/ui/server/held-actions.ts +35 -7
- package/src/ui/server/index.ts +20 -5
- package/src/ui/server/public/agents-page.js +33 -1
- package/src/ui/server/public/agents.html +13 -0
- package/src/ui/server/public/error-page.js +39 -1
- package/src/ui/server/public/graph.css +43 -0
- package/src/ui/server/public/graph.js +68 -1
- package/src/ui/server/public/header.js +36 -7
- package/src/ui/server/public/i18n/es.js +72 -0
- package/src/ui/server/public/i18n/ja.js +74 -0
- package/src/ui/server/public/local-dev.html +33 -3
- package/src/ui/server/public/log-source.js +26 -0
- package/src/ui/server/public/logs.html +7 -20
- package/src/ui/server/public/persona-ui.css +6 -0
- package/src/ui/server/public/server-agent-page.js +415 -0
- package/src/ui/server/public/server-agent.html +222 -0
- package/src/ui/server/public/services.html +18 -3
- package/src/ui/server/public/shell.js +274 -31
- package/src/ui/server/public/svc-panel.js +685 -0
- package/src/ui/server/public/topbar.js +13 -1
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/server-agent-routes.ts +176 -0
- package/src/ui/server/session-routes.ts +12 -1
- package/src/ui/server/ui-assets.ts +6 -0
- package/src/ui/server/workspace-routes.ts +3 -0
package/src/ui/server/index.ts
CHANGED
|
@@ -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,
|
|
@@ -858,6 +859,8 @@ import { heldActions } from "./held-actions.js";
|
|
|
858
859
|
import type { ApprovalQueue } from "./approvals.js";
|
|
859
860
|
import { processPathRules } from "../cli/onboard-reads.js";
|
|
860
861
|
import { handleAgentRoutes, startAgentUpkeep } from "./agent-routes.js";
|
|
862
|
+
import { handleServerAgentRoutes } from "./server-agent-routes.js";
|
|
863
|
+
import { freshDen } from "../cli/server-agent-ops.js";
|
|
861
864
|
import {
|
|
862
865
|
lastRunsByRepo,
|
|
863
866
|
recordRunEnd,
|
|
@@ -1768,6 +1771,8 @@ function approvalDeps(): {
|
|
|
1768
1771
|
validate: (m) => validateManifest(m),
|
|
1769
1772
|
workspaceCreateRefusal,
|
|
1770
1773
|
createWorkspace: (name) => createWorkspace(name, { services: {} }),
|
|
1774
|
+
activateWorkspace: (name) => activateWorkspace(name),
|
|
1775
|
+
workspaceActive: (name) => isWorkspaceActive(name),
|
|
1771
1776
|
workspaceExists: (name) =>
|
|
1772
1777
|
Object.prototype.hasOwnProperty.call(
|
|
1773
1778
|
readWorkspacesFile().workspaces ?? {},
|
|
@@ -1775,12 +1780,8 @@ function approvalDeps(): {
|
|
|
1775
1780
|
),
|
|
1776
1781
|
defaultWorkspace: () => getFocusedWorkspaceName() ?? "default",
|
|
1777
1782
|
// The exec route's checks for a local `up` (R13 placement, HITL).
|
|
1783
|
+
// An inactive workspace is fine: the Allow activates it (held-actions.ts).
|
|
1778
1784
|
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
1785
|
const resolved = resolveService(service, ws);
|
|
1785
1786
|
if (!resolved)
|
|
1786
1787
|
return {
|
|
@@ -2140,9 +2141,18 @@ async function handleRequest(
|
|
|
2140
2141
|
res.end();
|
|
2141
2142
|
return;
|
|
2142
2143
|
}
|
|
2144
|
+
// ON-6: a fresh Den's first page is its own agent (no redirect: a
|
|
2145
|
+
// setup link's #ak= fragment stays on this address).
|
|
2146
|
+
if (pathname === "/" && (await freshDen()))
|
|
2147
|
+
return serveFile(res, path.join(PUBLIC_DIR, "server-agent.html"));
|
|
2143
2148
|
return serveFile(res, path.join(PUBLIC_DIR, "local-dev.html"));
|
|
2144
2149
|
}
|
|
2145
2150
|
|
|
2151
|
+
// The server agent's page (ON-6): this Den's own agent, in a terminal.
|
|
2152
|
+
if (pathname === "/server-agent" || pathname === "/server-agent.html") {
|
|
2153
|
+
return serveFile(res, path.join(PUBLIC_DIR, "server-agent.html"));
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2146
2156
|
// The Graph page (R22): Local Dev's page in its full-width graph view.
|
|
2147
2157
|
if (pathname === "/graph" || pathname === "/graph.html") {
|
|
2148
2158
|
if (!isLocalEnabled()) {
|
|
@@ -4284,6 +4294,9 @@ async function handleRequest(
|
|
|
4284
4294
|
uptimeMs: Date.now() - SERVER_STARTED_AT,
|
|
4285
4295
|
stale,
|
|
4286
4296
|
scanned,
|
|
4297
|
+
// ON-6: no repo in any workspace yet (the server agent page's own
|
|
4298
|
+
// `fresh`): the shell skips What's new on a brand-new Den.
|
|
4299
|
+
fresh: await freshDen(),
|
|
4287
4300
|
});
|
|
4288
4301
|
}
|
|
4289
4302
|
|
|
@@ -4587,6 +4600,8 @@ async function handleRequest(
|
|
|
4587
4600
|
return;
|
|
4588
4601
|
}
|
|
4589
4602
|
|
|
4603
|
+
// ── The server agent (ON-6, server-agent-routes.ts) ──────────────────────
|
|
4604
|
+
if (await handleServerAgentRoutes(req, res, pathname)) return;
|
|
4590
4605
|
// ── Agents on the Workspaces page (AG-4a, agent-routes.ts) ───────────────
|
|
4591
4606
|
if (await handleAgentRoutes(req, res, pathname)) return;
|
|
4592
4607
|
// ── The Agents tab's list (AG-4a-term, session-routes.ts) ────────────────
|
|
@@ -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
|
|
|
@@ -203,6 +205,7 @@ function writeUrl() {
|
|
|
203
205
|
else u.searchParams.delete("open");
|
|
204
206
|
u.searchParams.set("layout", st.layout);
|
|
205
207
|
u.searchParams.delete("start");
|
|
208
|
+
u.searchParams.delete("folder");
|
|
206
209
|
const s = u.searchParams.toString().replace(/%2C/gi, ",");
|
|
207
210
|
history.replaceState(history.state, "", u.pathname + (s ? `?${s}` : ""));
|
|
208
211
|
paintSub();
|
|
@@ -338,6 +341,9 @@ function wsHref(ws) {
|
|
|
338
341
|
|
|
339
342
|
/** Workspace `ws` as a link to it on Workspaces. */
|
|
340
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>`;
|
|
341
347
|
return `<a class="wslink" href="${esc(wsHref(ws))}" title="${esc(t("ag.goWs", { ws }))}">${esc(ws)}</a>`;
|
|
342
348
|
}
|
|
343
349
|
|
|
@@ -454,7 +460,10 @@ function paintList() {
|
|
|
454
460
|
el.innerHTML = notes.join("");
|
|
455
461
|
return;
|
|
456
462
|
}
|
|
457
|
-
|
|
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();
|
|
458
467
|
// ?ws= names a workspace that isn't active: say so, and offer the switch.
|
|
459
468
|
const asked = st.data.requested;
|
|
460
469
|
if (asked && !asked.active)
|
|
@@ -514,6 +523,7 @@ function paintList() {
|
|
|
514
523
|
|
|
515
524
|
function visibleRows() {
|
|
516
525
|
const shown = new Set(viewWorkspaces());
|
|
526
|
+
shown.add(SERVER_WS);
|
|
517
527
|
return (st.data?.sessions ?? []).filter((s) => shown.has(s.workspace));
|
|
518
528
|
}
|
|
519
529
|
|
|
@@ -2101,8 +2111,16 @@ function dialogMode(bring, change = null) {
|
|
|
2101
2111
|
);
|
|
2102
2112
|
$d("#ag-go").textContent = goLabel();
|
|
2103
2113
|
$d("#ag-conv-f").hidden = !bring;
|
|
2114
|
+
// 0.4.18: a new agent's folder (Start agent here fills it).
|
|
2115
|
+
for (const id of ["#ag-folder-l", "#ag-folder-h"]) {
|
|
2116
|
+
const el = $d(id);
|
|
2117
|
+
if (el) el.hidden = !!(bring || change);
|
|
2118
|
+
}
|
|
2104
2119
|
}
|
|
2105
2120
|
|
|
2121
|
+
/** 0.4.18: Start agent here (the service panel): ?start=1&ws=…&folder=…. */
|
|
2122
|
+
let startPrefill = null;
|
|
2123
|
+
|
|
2106
2124
|
/** The submit button's words for the dialog's mode. */
|
|
2107
2125
|
function goLabel() {
|
|
2108
2126
|
return t(sd.change ? "ag.pc.go" : sd.bring ? "ag.bring.go" : "ag.go");
|
|
@@ -2124,12 +2142,18 @@ async function openStart(bring = false, change = null) {
|
|
|
2124
2142
|
const dlg = $d("#ag-dlg");
|
|
2125
2143
|
if (!dlg || dlg.open) return;
|
|
2126
2144
|
dialogMode(bring, change);
|
|
2145
|
+
const pre = change || bring ? null : startPrefill;
|
|
2146
|
+
startPrefill = null;
|
|
2127
2147
|
const shown = change ? [change.workspace] : viewWorkspaces();
|
|
2148
|
+
if (pre?.ws && !shown.includes(pre.ws)) shown.push(pre.ws);
|
|
2128
2149
|
const wsSel = $d("#ag-ws");
|
|
2129
2150
|
wsSel.innerHTML = shown
|
|
2130
2151
|
.map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
|
|
2131
2152
|
.join("");
|
|
2132
2153
|
wsSel.disabled = shown.length <= 1;
|
|
2154
|
+
if (pre?.ws) wsSel.value = pre.ws;
|
|
2155
|
+
const folder = $d("#ag-folder");
|
|
2156
|
+
if (folder) folder.value = pre?.folder ?? "";
|
|
2133
2157
|
sd.f = { q: "", cat: "", src: "" };
|
|
2134
2158
|
sd.det = false;
|
|
2135
2159
|
sd.what = false;
|
|
@@ -2170,6 +2194,8 @@ function startBody() {
|
|
|
2170
2194
|
if (task) body.instruction = task;
|
|
2171
2195
|
const name = $d("#ag-name").value.trim();
|
|
2172
2196
|
if (name) body.name = name;
|
|
2197
|
+
const folder = $d("#ag-folder")?.value.trim();
|
|
2198
|
+
if (folder) body.folder = folder;
|
|
2173
2199
|
return body;
|
|
2174
2200
|
}
|
|
2175
2201
|
|
|
@@ -2628,6 +2654,8 @@ function translateStatic(root) {
|
|
|
2628
2654
|
set("#ag-what-p", "pe.what");
|
|
2629
2655
|
set("#ag-task-t", "ag.new.task");
|
|
2630
2656
|
set("#ag-name-t", "ag.new.name");
|
|
2657
|
+
set("#ag-folder-t", "ag.new.folder");
|
|
2658
|
+
set("#ag-folder-h", "ag.new.folderHelp");
|
|
2631
2659
|
set("#ag-adv-s", "ag.new.adv");
|
|
2632
2660
|
set("#ag-note", "ag.new.note");
|
|
2633
2661
|
const task = root.querySelector("#ag-task");
|
|
@@ -2659,6 +2687,10 @@ export async function mountAgentsPage(root) {
|
|
|
2659
2687
|
const wanted = openFromUrl(location.search);
|
|
2660
2688
|
const linked = layoutFromUrl(location.search);
|
|
2661
2689
|
const startAsked = new URLSearchParams(location.search).get("start") === "1";
|
|
2690
|
+
if (startAsked) {
|
|
2691
|
+
const q = new URLSearchParams(location.search);
|
|
2692
|
+
startPrefill = { ws: q.get("ws") || "", folder: q.get("folder") || "" };
|
|
2693
|
+
}
|
|
2662
2694
|
|
|
2663
2695
|
root.querySelector("#agl").addEventListener("click", (e) => {
|
|
2664
2696
|
const act = e.target.closest("button[data-act]");
|
|
@@ -739,6 +739,19 @@
|
|
|
739
739
|
><span id="ag-name-t">Name (optional)</span>
|
|
740
740
|
<input id="ag-name" maxlength="40" autocomplete="off" />
|
|
741
741
|
</label>
|
|
742
|
+
<label class="fld" id="ag-folder-l"
|
|
743
|
+
><span id="ag-folder-t">Folder (optional)</span>
|
|
744
|
+
<input
|
|
745
|
+
id="ag-folder"
|
|
746
|
+
maxlength="4096"
|
|
747
|
+
autocomplete="off"
|
|
748
|
+
spellcheck="false"
|
|
749
|
+
aria-describedby="ag-folder-h"
|
|
750
|
+
/>
|
|
751
|
+
</label>
|
|
752
|
+
<p class="helpl" id="ag-folder-h">
|
|
753
|
+
Inside the workspace's folder. Empty: the workspace's own.
|
|
754
|
+
</p>
|
|
742
755
|
<p class="helpl" id="ag-as" role="status"></p>
|
|
743
756
|
<details id="ag-adv">
|
|
744
757
|
<summary id="ag-adv-s">Advanced: model and permissions</summary>
|
|
@@ -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);
|
|
@@ -349,6 +370,28 @@
|
|
|
349
370
|
color: var(--accent-text);
|
|
350
371
|
background: var(--sunken);
|
|
351
372
|
}
|
|
373
|
+
/* 0.4.18: ⋯ opens the service panel (svc-panel.js), as a pinned row does.
|
|
374
|
+
It sits before the star, which stays top-right. */
|
|
375
|
+
.dg-svp {
|
|
376
|
+
margin-left: auto;
|
|
377
|
+
flex: none;
|
|
378
|
+
background: none;
|
|
379
|
+
border: 1px solid transparent;
|
|
380
|
+
border-radius: 6px;
|
|
381
|
+
color: var(--faint);
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
font-size: 13px;
|
|
384
|
+
line-height: 1;
|
|
385
|
+
padding: 1px 4px;
|
|
386
|
+
}
|
|
387
|
+
.dg-svp + .dg-star {
|
|
388
|
+
margin-left: 0;
|
|
389
|
+
}
|
|
390
|
+
.dg-svp:hover,
|
|
391
|
+
.dg-svp[aria-expanded="true"] {
|
|
392
|
+
color: var(--accent-text);
|
|
393
|
+
background: var(--sunken);
|
|
394
|
+
}
|
|
352
395
|
.dg-dot {
|
|
353
396
|
display: inline-block;
|
|
354
397
|
width: 9px;
|
|
@@ -256,9 +256,69 @@ export function mountGraph(el, data, pageHooks) {
|
|
|
256
256
|
st.fitted = true;
|
|
257
257
|
} else applyView();
|
|
258
258
|
if (st.sel) openDetail(st.sel, true);
|
|
259
|
+
focusFromHash();
|
|
259
260
|
});
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
/** 0.4.18: the panel's Open in Graph (#focus=<repo> or <repo>/<service>), once. */
|
|
264
|
+
let hashFocused = false;
|
|
265
|
+
/** The id #focus= names, or "" (none, or not decodable). */
|
|
266
|
+
function hashFocusId() {
|
|
267
|
+
if (!location.hash.startsWith("#focus=")) return "";
|
|
268
|
+
try {
|
|
269
|
+
return decodeURIComponent(location.hash.slice(7));
|
|
270
|
+
} catch {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function focusFromHash() {
|
|
275
|
+
if (hashFocused) return;
|
|
276
|
+
const id = hashFocusId();
|
|
277
|
+
if (!id) return;
|
|
278
|
+
host.querySelector(".dg-out")?.remove();
|
|
279
|
+
if (M.byId.has(id)) focusOn({ id });
|
|
280
|
+
else if (M.byRepo.has(id)) focusOn({ repo: id, viaRepo: true });
|
|
281
|
+
else return outsideNote(id);
|
|
282
|
+
hashFocused = true;
|
|
283
|
+
}
|
|
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
|
+
|
|
262
322
|
/** A /api/health update: refresh dots, health lines and the open panel in place. */
|
|
263
323
|
export function graphHealth(results) {
|
|
264
324
|
if (!M || !host?.isConnected) return;
|
|
@@ -511,6 +571,13 @@ function starBtn(id) {
|
|
|
511
571
|
: `${on ? "Unpin" : "Pin"} ${id}`;
|
|
512
572
|
return `<button type="button" class="dg-star" data-pin-toggle="${esc(id)}" aria-pressed="${on}" aria-label="${esc(label)}" title="${esc(label)}">${on ? "★" : "☆"}</button>`;
|
|
513
573
|
}
|
|
574
|
+
/** 0.4.18: ⋯ opens the service's quick panel (svc-panel.js), the rail's. */
|
|
575
|
+
function svpBtn(id) {
|
|
576
|
+
const sh = window.denShell;
|
|
577
|
+
if (!sh?.openServicePanel) return "";
|
|
578
|
+
const label = sh.t ? sh.t("svp.more", { s: id }) : id;
|
|
579
|
+
return `<button type="button" class="dg-svp" data-svpanel-open="${esc(id)}" aria-haspopup="dialog" aria-expanded="false" aria-label="${esc(label)}" title="${esc(label)}">⋯</button>`;
|
|
580
|
+
}
|
|
514
581
|
/** A card's actions: the first three, and Visit ↗ always when it has a URL. */
|
|
515
582
|
function cardActs(s) {
|
|
516
583
|
const acts = actionsFor(s);
|
|
@@ -522,7 +589,7 @@ function cardActs(s) {
|
|
|
522
589
|
}
|
|
523
590
|
function cardInner(s) {
|
|
524
591
|
const showPlace = s.runs === "remote" || M.mixed;
|
|
525
|
-
return `<div class="r1"><span class="dg-dot ${status(s)}" title="${statusWord(s)}"></span><span class="nm" title="${esc(s.name)}">${esc(s.name)}</span>${showPlace ? `<span class="dg-badge ${s.runs}">${esc(placementText(s))}</span>` : ""}${starBtn(s.id)}</div>
|
|
592
|
+
return `<div class="r1"><span class="dg-dot ${status(s)}" title="${statusWord(s)}"></span><span class="nm" title="${esc(s.name)}">${esc(s.name)}</span>${showPlace ? `<span class="dg-badge ${s.runs}">${esc(placementText(s))}</span>` : ""}${svpBtn(s.id)}${starBtn(s.id)}</div>
|
|
526
593
|
<div class="r2" title="${esc(s.id)}${s.inWorkspaces.length > 1 ? `: in ${esc(s.inWorkspaces.join(", "))}` : ""}">${esc(s.repo + ownersLabel(s.inWorkspaces))}</div>
|
|
527
594
|
<div class="r3">${s.entry ? '<span class="dg-tag entry">entry</span>' : ""}${s.infra ? '<span class="dg-tag">infra</span>' : ""}${s.missing.length ? `<span class="dg-tag warn" title="Missing: ${esc(s.missing.join(", "))}">⚠ missing</span>` : ""}${!s.cloned ? '<span class="dg-tag warn">not cloned</span>' : ""}<span class="h ${status(s)}">${esc(healthLine(s))}</span></div>
|
|
528
595
|
<div class="dg-acts">${cardActs(s)
|
|
@@ -17,6 +17,7 @@ import { changelogView } from "./changelog-view.js";
|
|
|
17
17
|
import { buildRow, healthCycle, paintGate, shellHealth } from "./health-fit.js";
|
|
18
18
|
import { initTopbar, paintChecking, paintStatus } from "./topbar.js";
|
|
19
19
|
import { registerDenSettings } from "./settings-registry.js";
|
|
20
|
+
import { initServicePanel } from "./svc-panel.js";
|
|
20
21
|
|
|
21
22
|
/** The shell's message lookup (R19; shell.js MESSAGES). */
|
|
22
23
|
const t = (key, vars) =>
|
|
@@ -54,6 +55,12 @@ if (typeof document !== "undefined")
|
|
|
54
55
|
healthEnv: () => healthEnv(),
|
|
55
56
|
storeOpenLog: (repo, service, env) => storeOpenLog(repo, service, env),
|
|
56
57
|
});
|
|
58
|
+
// 0.4.18: the service panel (rail pins, Graph ⋯, Services names).
|
|
59
|
+
if (typeof document !== "undefined")
|
|
60
|
+
initServicePanel({
|
|
61
|
+
currentEnv: () => currentEnv(),
|
|
62
|
+
refreshHealth: (env, opts) => refreshHealth(env, opts),
|
|
63
|
+
});
|
|
57
64
|
|
|
58
65
|
// 5s between polls — measured from each poll's COMPLETION (schedulePoll
|
|
59
66
|
// re-arms after renderHealth resolves), so slow probes can't overlap.
|
|
@@ -643,15 +650,15 @@ export async function initChangelog() {
|
|
|
643
650
|
}
|
|
644
651
|
|
|
645
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
|
+
}
|
|
646
659
|
const pill = document.getElementById("version-pill");
|
|
647
660
|
if (pill) {
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
running = (await fetch("/api/server-info").then((r) => r.json())).version;
|
|
651
|
-
} catch {
|
|
652
|
-
/* fall back to the changelog's newest version */
|
|
653
|
-
}
|
|
654
|
-
const label = versionLabel(running, _changelogEntries);
|
|
661
|
+
const label = versionLabel(info?.version ?? null, _changelogEntries);
|
|
655
662
|
if (label) pill.querySelector(".v-txt").textContent = label;
|
|
656
663
|
}
|
|
657
664
|
|
|
@@ -660,6 +667,16 @@ export async function initChangelog() {
|
|
|
660
667
|
lastSeen = localStorage.getItem(LS_CHANGELOG_SEEN);
|
|
661
668
|
} catch {}
|
|
662
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
|
+
|
|
663
680
|
if (latest?.version && lastSeen !== latest.version) {
|
|
664
681
|
const pip = document.getElementById("v-pip");
|
|
665
682
|
if (pip) pip.style.display = "block";
|
|
@@ -895,6 +912,18 @@ async function _switchScope(value, focus, inView) {
|
|
|
895
912
|
location.reload();
|
|
896
913
|
}
|
|
897
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
|
+
|
|
898
927
|
/**
|
|
899
928
|
* Make `name` the CLI default: Local Dev's focus action (POST
|
|
900
929
|
* /api/workspaces `focus`, which writes workspaces.yml and retargets the
|
|
@@ -55,6 +55,43 @@
|
|
|
55
55
|
"act.unpin": "Quitar",
|
|
56
56
|
"act.visit": "Visitar",
|
|
57
57
|
"act.visitX": "Visitar {s} (se abre en una pestaña nueva)",
|
|
58
|
+
// 0.4.18: the service panel (svc-panel.js) and Start's Folder.
|
|
59
|
+
"svp.label": "{s}: panel rápido",
|
|
60
|
+
"svp.more": "Panel rápido de {s}",
|
|
61
|
+
"svp.notRunning": "No está en marcha",
|
|
62
|
+
"svp.up": "activo {d}",
|
|
63
|
+
"svp.upNow": "activo",
|
|
64
|
+
"svp.down": "caído: {c}",
|
|
65
|
+
"svp.downNone": "caído",
|
|
66
|
+
"svp.starting": "iniciando…",
|
|
67
|
+
"svp.stopped": "detenido",
|
|
68
|
+
"svp.noUrl": "Sin URL",
|
|
69
|
+
"svp.start": "Iniciar",
|
|
70
|
+
"svp.stop": "Detener",
|
|
71
|
+
"svp.restart": "Reiniciar",
|
|
72
|
+
"svp.logs": "Registros",
|
|
73
|
+
"svp.stopping": "Deteniendo {s}",
|
|
74
|
+
"svp.tests": "Última ejecución de pruebas",
|
|
75
|
+
"svp.testsOk": "✓ {t}",
|
|
76
|
+
"svp.testsBad": "✗ falló {t}",
|
|
77
|
+
"svp.testsRun": "en curso…",
|
|
78
|
+
"svp.testsNone": "—",
|
|
79
|
+
"svp.runTests": "Ejecutar pruebas",
|
|
80
|
+
"svp.testsDone": "Pruebas superadas: {s}",
|
|
81
|
+
"svp.testsFailed": "Pruebas fallidas: {s}",
|
|
82
|
+
"svp.agents": "Agentes",
|
|
83
|
+
"svp.noAgents": "No hay agentes en este repo.",
|
|
84
|
+
"svp.open": "Abrir",
|
|
85
|
+
"svp.openAg": "Abrir {s}",
|
|
86
|
+
"svp.startHere": "Iniciar agente aquí",
|
|
87
|
+
"svp.openSvc": "Abrir en Servicios",
|
|
88
|
+
"svp.openGraph": "Abrir en Grafo",
|
|
89
|
+
"graph.outside": "{s} está en {ws}, fuera de esta vista.",
|
|
90
|
+
"graph.showWs": "Mostrar {ws}",
|
|
91
|
+
"svp.close": "Cerrar",
|
|
92
|
+
"ag.new.folder": "Carpeta (opcional)",
|
|
93
|
+
"ag.new.folderHelp":
|
|
94
|
+
"Dentro de la carpeta del espacio de trabajo. Vacío: la suya.",
|
|
58
95
|
"act.pinX": "Fijar {s}",
|
|
59
96
|
"act.unpinX": "Quitar {s}",
|
|
60
97
|
"toast.pinned": "{s} fijado",
|
|
@@ -278,6 +315,10 @@
|
|
|
278
315
|
"err.signin.step2":
|
|
279
316
|
"Esta página inicia sesión sola una vez aprobada. El código caduca en 10 minutos.",
|
|
280
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.",
|
|
281
322
|
"err.signin.noTerminal":
|
|
282
323
|
"¿No tienes una terminal en {host}? Desde este ordenador, ejecuta:",
|
|
283
324
|
"err.signin.openTerminal": "Abrir Terminal",
|
|
@@ -545,6 +586,8 @@
|
|
|
545
586
|
"ap.do.repo_add.new":
|
|
546
587
|
'Añadir el repositorio de {path} a un espacio nuevo "{workspace}"',
|
|
547
588
|
"ap.do.service_try": "Iniciar {service}",
|
|
589
|
+
"ap.do.service_try.activates":
|
|
590
|
+
"Iniciar {service} (esto activa {workspace})",
|
|
548
591
|
"ap.says": "El agente dice:",
|
|
549
592
|
"ap.review": "Revisar",
|
|
550
593
|
"ap.hide": "Ocultar detalles",
|
|
@@ -689,6 +732,35 @@
|
|
|
689
732
|
"ag.ws": "Espacio de trabajo",
|
|
690
733
|
"ag.pal": "Agentes › {ws} / {name}",
|
|
691
734
|
"ag.palStart": "Iniciar agente…",
|
|
735
|
+
// ON-6: the server agent (server-agent-page.js, the rail, ⌘K).
|
|
736
|
+
"n.serverAgent": "Agente de __DEN_TITLE__",
|
|
737
|
+
"ag.server": "Servidor",
|
|
738
|
+
"pal.askServer": "Preguntar a __DEN_TITLE__",
|
|
739
|
+
"sa.sub.fresh":
|
|
740
|
+
"Vamos a configurar __DEN_TITLE__. Solo lee hasta que permitas un cambio.",
|
|
741
|
+
"sa.sub":
|
|
742
|
+
"Para todo lo del servidor: añadir un repo, arreglar un servicio que falla, cambiar un ajuste o explicar qué está en marcha.",
|
|
743
|
+
"sa.trust": "Claude pregunta si confías en esta carpeta. Elige Sí.",
|
|
744
|
+
"sa.model": "Modelo",
|
|
745
|
+
"sa.login":
|
|
746
|
+
"Si Claude te pide iniciar sesión, hazlo aquí. Después {name} empieza.",
|
|
747
|
+
"sa.readsOnly":
|
|
748
|
+
"Solo actúa con las herramientas de __DEN_TITLE__. Cada cambio te lo pregunta antes, en este navegador.",
|
|
749
|
+
"sa.starting": "Iniciando el agente…",
|
|
750
|
+
"sa.idle": "{name} no está en marcha.",
|
|
751
|
+
"sa.start": "Iniciar",
|
|
752
|
+
"sa.failed": "No se inició: {why}. {cli} agents doctor da más detalles.",
|
|
753
|
+
"sa.unread":
|
|
754
|
+
"No se pudo leer el estado del agente. ¿Está en marcha el servidor web?",
|
|
755
|
+
"sa.none.h": "Aún no hay ningún runtime para el agente",
|
|
756
|
+
"sa.none":
|
|
757
|
+
"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.",
|
|
758
|
+
"sa.noTools": "aún no puede usar las herramientas de __DEN_TITLE__",
|
|
759
|
+
"sa.manual.h": "O configúralo tú",
|
|
760
|
+
"sa.manual": "Cada paso del agente también es un comando. En un terminal:",
|
|
761
|
+
"sa.runsOn": "Se ejecuta en",
|
|
762
|
+
"sa.openAgents": "Abrir en Agentes",
|
|
763
|
+
"sa.saved": "Guardado. El agente lo usa desde su próximo inicio.",
|
|
692
764
|
"ag.cap":
|
|
693
765
|
"Hasta 4 paneles: se cerró el de {id} para hacer sitio (sigue en marcha)",
|
|
694
766
|
"ag.layout": "Disposición",
|