@monster-orange/den 0.4.18 → 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 +134 -0
- package/README.md +14 -13
- package/docs/distributions.md +99 -24
- 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 +41 -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 +1 -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 +10 -1
- package/src/ui/server/public/error-page.js +39 -1
- package/src/ui/server/public/graph.css +21 -0
- package/src/ui/server/public/graph.js +49 -6
- package/src/ui/server/public/header.js +29 -7
- package/src/ui/server/public/i18n/es.js +37 -0
- package/src/ui/server/public/i18n/ja.js +39 -0
- package/src/ui/server/public/local-dev.html +22 -1
- 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/shell.js +66 -6
- package/src/ui/server/public/svc-panel.js +27 -14
- 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 +2 -0
- package/src/ui/server/workspace-routes.ts +3 -0
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
* copies:
|
|
6
6
|
*
|
|
7
7
|
* workspace_create the Workspaces page's create (workspaceCreateRefusal,
|
|
8
|
-
* createWorkspace)
|
|
8
|
+
* createWorkspace), then activates it: nothing an
|
|
9
|
+
* agent calls activates a workspace, and service_try
|
|
10
|
+
* needs an active one
|
|
9
11
|
* repo_add `repo add --json` (repo-command.ts repoAdd), a local
|
|
10
12
|
* folder inside home only, as manifest_draft reads; its
|
|
11
13
|
* dry run is the Review
|
|
12
14
|
* service_try `up` through the service runner, as
|
|
13
15
|
* /api/service/exec and service_up run it
|
|
14
|
-
* (lifecycle-exec.ts), until it is ready or exits
|
|
16
|
+
* (lifecycle-exec.ts), until it is ready or exits;
|
|
17
|
+
* an inactive workspace is activated first, on the
|
|
18
|
+
* same Allow, and the Review says so
|
|
15
19
|
*/
|
|
16
20
|
import * as path from "path";
|
|
17
21
|
import { checkOnboardPath, type PathRules } from "../cli/onboard-reads.js";
|
|
@@ -35,6 +39,8 @@ export interface HeldActionDeps {
|
|
|
35
39
|
validate: ManifestValidator;
|
|
36
40
|
workspaceCreateRefusal: (name: string | undefined) => string | null;
|
|
37
41
|
createWorkspace: (name: string) => void;
|
|
42
|
+
activateWorkspace: (name: string) => void;
|
|
43
|
+
workspaceActive: (name: string) => boolean;
|
|
38
44
|
workspaceExists: (name: string) => boolean;
|
|
39
45
|
/** Where `repo add` puts a repo with no -w: the focused one, else `default`. */
|
|
40
46
|
defaultWorkspace: () => string;
|
|
@@ -84,13 +90,16 @@ export function heldActions(d: HeldActionDeps): HeldActions {
|
|
|
84
90
|
args: { name },
|
|
85
91
|
summary: `create the workspace "${name}"`,
|
|
86
92
|
params: { name },
|
|
87
|
-
details: [
|
|
93
|
+
details: [
|
|
94
|
+
`A new, empty workspace named "${name}", made active so its services can start.`,
|
|
95
|
+
],
|
|
88
96
|
};
|
|
89
97
|
},
|
|
90
98
|
async run(a) {
|
|
91
99
|
const name = String(a.name);
|
|
92
100
|
d.createWorkspace(name);
|
|
93
|
-
|
|
101
|
+
d.activateWorkspace(name);
|
|
102
|
+
return { ok: true, name, active: true };
|
|
94
103
|
},
|
|
95
104
|
};
|
|
96
105
|
|
|
@@ -177,21 +186,40 @@ export function heldActions(d: HeldActionDeps): HeldActions {
|
|
|
177
186
|
if (!service)
|
|
178
187
|
return { ok: false, error: "give service: <repo> or <repo>/<service>" };
|
|
179
188
|
const workspace = str(a.workspace);
|
|
189
|
+
if (workspace !== undefined && !NAME_RE.test(workspace))
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: `workspace "${workspace}" must use only letters, digits, ".", "_" and "-"`,
|
|
193
|
+
};
|
|
194
|
+
if (workspace !== undefined && !d.workspaceExists(workspace))
|
|
195
|
+
return { ok: false, error: `there is no workspace "${workspace}"` };
|
|
180
196
|
const t = d.serviceTarget(service, workspace);
|
|
181
197
|
if (!t.ok) return { ok: false, error: t.error };
|
|
198
|
+
const activates =
|
|
199
|
+
workspace !== undefined && !d.workspaceActive(workspace);
|
|
182
200
|
return {
|
|
183
201
|
ok: true,
|
|
184
202
|
args: { service: t.address, ...(workspace ? { workspace } : {}) },
|
|
185
|
-
summary: `start ${t.address}${workspace ? ` in workspace "${workspace}"` : ""}`,
|
|
186
|
-
params: {
|
|
203
|
+
summary: `start ${t.address}${workspace ? ` in workspace "${workspace}"` : ""}${activates ? ` (this activates ${workspace})` : ""}`,
|
|
204
|
+
params: {
|
|
205
|
+
service: t.address,
|
|
206
|
+
...(workspace ? { workspace } : {}),
|
|
207
|
+
...(activates ? { activates: "1" } : {}),
|
|
208
|
+
},
|
|
187
209
|
details: [
|
|
210
|
+
...(activates
|
|
211
|
+
? [`Activates the workspace "${workspace}" first, so it can run.`]
|
|
212
|
+
: []),
|
|
188
213
|
`Runs the service's local "up", as the Services page's Start does.`,
|
|
189
214
|
`Den reports the exit code and the last lines of its output.`,
|
|
190
215
|
],
|
|
191
216
|
};
|
|
192
217
|
},
|
|
193
218
|
async run(a) {
|
|
194
|
-
|
|
219
|
+
const workspace = str(a.workspace);
|
|
220
|
+
if (workspace && !d.workspaceActive(workspace))
|
|
221
|
+
d.activateWorkspace(workspace);
|
|
222
|
+
return d.tryService(String(a.service), workspace);
|
|
195
223
|
},
|
|
196
224
|
};
|
|
197
225
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
/** The id #focus= names, or "" (none, or not decodable). */
|
|
266
|
+
function hashFocusId() {
|
|
267
|
+
if (!location.hash.startsWith("#focus=")) return "";
|
|
268
268
|
try {
|
|
269
|
-
|
|
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
|
-
|
|
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",
|
|
@@ -580,6 +586,8 @@
|
|
|
580
586
|
"ap.do.repo_add.new":
|
|
581
587
|
'Añadir el repositorio de {path} a un espacio nuevo "{workspace}"',
|
|
582
588
|
"ap.do.service_try": "Iniciar {service}",
|
|
589
|
+
"ap.do.service_try.activates":
|
|
590
|
+
"Iniciar {service} (esto activa {workspace})",
|
|
583
591
|
"ap.says": "El agente dice:",
|
|
584
592
|
"ap.review": "Revisar",
|
|
585
593
|
"ap.hide": "Ocultar detalles",
|
|
@@ -724,6 +732,35 @@
|
|
|
724
732
|
"ag.ws": "Espacio de trabajo",
|
|
725
733
|
"ag.pal": "Agentes › {ws} / {name}",
|
|
726
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.",
|
|
727
764
|
"ag.cap":
|
|
728
765
|
"Hasta 4 paneles: se cerró el de {id} para hacer sitio (sigue en marcha)",
|
|
729
766
|
"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": "ターミナルを開く",
|
|
@@ -566,6 +572,8 @@
|
|
|
566
572
|
"ap.do.repo_add.new":
|
|
567
573
|
"{path} のリポジトリを新しいワークスペース「{workspace}」に追加",
|
|
568
574
|
"ap.do.service_try": "{service} を起動",
|
|
575
|
+
"ap.do.service_try.activates":
|
|
576
|
+
"{service} を起動 ({workspace} を有効にします)",
|
|
569
577
|
"ap.says": "エージェントのコメント:",
|
|
570
578
|
"ap.review": "確認",
|
|
571
579
|
"ap.hide": "詳細を隠す",
|
|
@@ -707,6 +715,37 @@
|
|
|
707
715
|
"ag.ws": "ワークスペース",
|
|
708
716
|
"ag.pal": "エージェント › {ws} / {name}",
|
|
709
717
|
"ag.palStart": "エージェントを開始…",
|
|
718
|
+
// ON-6: the server agent (server-agent-page.js, the rail, ⌘K).
|
|
719
|
+
"n.serverAgent": "__DEN_TITLE__ エージェント",
|
|
720
|
+
"ag.server": "サーバー",
|
|
721
|
+
"pal.askServer": "__DEN_TITLE__ に聞く",
|
|
722
|
+
"sa.sub.fresh":
|
|
723
|
+
"__DEN_TITLE__ をセットアップしましょう。変更を許可するまで、読み取りだけを行います。",
|
|
724
|
+
"sa.sub":
|
|
725
|
+
"サーバーのことなら何でも:リポジトリの追加、失敗しているサービスの修正、設定の変更、動いているものの説明。",
|
|
726
|
+
"sa.trust":
|
|
727
|
+
"Claude がこのフォルダーを信頼するか尋ねます。「はい」を選んでください。",
|
|
728
|
+
"sa.model": "モデル",
|
|
729
|
+
"sa.login":
|
|
730
|
+
"Claude からログインを求められたら、ここでログインしてください。そのあと {name} が始まります。",
|
|
731
|
+
"sa.readsOnly":
|
|
732
|
+
"__DEN_TITLE__ 自身のツールだけで動きます。変更はそのたびに、このブラウザーで先に確認します。",
|
|
733
|
+
"sa.starting": "エージェントを起動しています…",
|
|
734
|
+
"sa.idle": "{name} は動いていません。",
|
|
735
|
+
"sa.start": "開始",
|
|
736
|
+
"sa.failed":
|
|
737
|
+
"起動しませんでした:{why}。詳しくは {cli} agents doctor で確認できます。",
|
|
738
|
+
"sa.unread":
|
|
739
|
+
"エージェントの状態を読めませんでした。Web サーバーは動いていますか?",
|
|
740
|
+
"sa.none.h": "エージェントを動かせるランタイムがまだありません",
|
|
741
|
+
"sa.none":
|
|
742
|
+
"エージェントはランタイムカタログのエージェント CLI で動きます。1 つインストールして一度ログインし、このページを再読み込みしてください。",
|
|
743
|
+
"sa.noTools": "まだ __DEN_TITLE__ のツールを使えません",
|
|
744
|
+
"sa.manual.h": "自分でセットアップする",
|
|
745
|
+
"sa.manual": "エージェントの各手順はコマンドでもできます。ターミナルで:",
|
|
746
|
+
"sa.runsOn": "実行するランタイム",
|
|
747
|
+
"sa.openAgents": "エージェントで開く",
|
|
748
|
+
"sa.saved": "保存しました。次回の起動から使われます。",
|
|
710
749
|
"ag.cap":
|
|
711
750
|
"ペインは最大 4 つです。{id} のペインを閉じました(実行は続きます)",
|
|
712
751
|
"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 {
|
|
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;
|