@monster-orange/den 0.4.17 → 0.4.18
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 +43 -0
- package/README.md +7 -7
- package/docs/distributions.md +39 -1
- package/package.json +1 -1
- package/src/ui/cli/agent-ops.ts +43 -0
- package/src/ui/server/agent-routes.ts +9 -0
- package/src/ui/server/public/agents-page.js +23 -0
- package/src/ui/server/public/agents.html +13 -0
- package/src/ui/server/public/graph.css +22 -0
- package/src/ui/server/public/graph.js +25 -1
- package/src/ui/server/public/header.js +7 -0
- package/src/ui/server/public/i18n/es.js +35 -0
- package/src/ui/server/public/i18n/ja.js +35 -0
- package/src/ui/server/public/local-dev.html +11 -2
- package/src/ui/server/public/log-source.js +26 -0
- package/src/ui/server/public/logs.html +7 -20
- package/src/ui/server/public/services.html +18 -3
- package/src/ui/server/public/shell.js +208 -25
- package/src/ui/server/public/svc-panel.js +672 -0
- package/src/ui/server/ui-assets.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ All notable changes to Den are documented here. Each release lists the
|
|
|
4
4
|
config and manifest changes a distribution must make to move to it under
|
|
5
5
|
**Migrations**.
|
|
6
6
|
|
|
7
|
+
## [0.4.18] - 2026-11-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **A pinned service opens a quick panel, not a page jump.** A click on a
|
|
12
|
+
pinned service's name in the rail (or Enter on its row, or its dot in
|
|
13
|
+
the collapsed rail) opens a panel beside the row, without leaving the
|
|
14
|
+
page. It has the service's workspace, state and URL; Visit ↗, Start or
|
|
15
|
+
Stop, Restart and Logs, which call what the Services page calls; the
|
|
16
|
+
repo's last test run with Run tests; the agents working in the repo,
|
|
17
|
+
each with Open, and Start agent here; and Open in Services, Open in
|
|
18
|
+
Graph and Unpin. It stays current while open. Esc or a click outside
|
|
19
|
+
closes it and focus goes back to the row. Under 640 px it is a bottom
|
|
20
|
+
sheet. ⌘-click or a middle click still opens Services in a new tab. The
|
|
21
|
+
Services card's name and the Graph card's new ⋯ open the same panel.
|
|
22
|
+
- **Visit ↗ on every pinned row.** Always shown, it opens the service's
|
|
23
|
+
URL in a new tab. It is greyed out ("Not running") while the service is
|
|
24
|
+
down, and absent when the service has no URL. The hover × is gone:
|
|
25
|
+
Unpin is in the panel, and Delete on the row still unpins.
|
|
26
|
+
- **Start agent here.** The Start dialog has a Folder field (optional):
|
|
27
|
+
the agent runs in that folder of the workspace's, or in a repo's
|
|
28
|
+
folder. The route takes it as `folder`, and refuses a folder outside
|
|
29
|
+
the workspace (403). The panel's Start agent here fills the workspace
|
|
30
|
+
and the repo's folder.
|
|
31
|
+
- **Open in Graph focuses the service.** `/graph#focus=<repo>` or
|
|
32
|
+
`#focus=<repo>/<service>` opens the Graph on that card.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **The panel's Logs reads the service's log.** It asks where the Logs
|
|
37
|
+
page asks (taillog for a local service), so a service with no `logs:`
|
|
38
|
+
block no longer says "Connection closed". The Services page's Logs
|
|
39
|
+
uses the same rule.
|
|
40
|
+
- **"starting…" follows health.** After Start or Restart, the panel
|
|
41
|
+
leaves "starting…" once health says up, once it says down after a
|
|
42
|
+
moment, or after two minutes. Restart works again then, even when the
|
|
43
|
+
service's `up` never exits (a dev server).
|
|
44
|
+
- **A pinned row's Visit ↗ greys out with the panel.** It reads the
|
|
45
|
+
panel's live state, so it greys out on the same health update, also
|
|
46
|
+
for a service outside the top bar's View.
|
|
47
|
+
- **Open in Graph keeps its target.** The Graph page keeps the URL's
|
|
48
|
+
`#focus=` when it picks a workspace, so the card is focused and in view.
|
|
49
|
+
|
|
7
50
|
## [0.4.17] - 2026-11-08
|
|
8
51
|
|
|
9
52
|
### Fixed
|
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<picture>
|
|
13
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.
|
|
14
|
-
<img src="docs/images/0.4.
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.18/hero-dark.png">
|
|
14
|
+
<img src="docs/images/0.4.18/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
|
|
15
15
|
</picture>
|
|
16
16
|
|
|
17
17
|
## Set up by your agent
|
|
@@ -38,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
|
|
|
38
38
|
yours. Den connects them and turns on good practice from the start.
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
41
|
-
<img src="docs/images/0.4.
|
|
41
|
+
<img src="docs/images/0.4.18/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
|
|
42
42
|
</p>
|
|
43
43
|
|
|
44
44
|
## How it helps
|
|
@@ -68,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
|
|
|
68
68
|
lives in your own repo.
|
|
69
69
|
|
|
70
70
|
<p align="center">
|
|
71
|
-
<img src="docs/images/0.4.
|
|
72
|
-
<img src="docs/images/0.4.
|
|
73
|
-
<img src="docs/images/0.4.
|
|
74
|
-
<img src="docs/images/0.4.
|
|
71
|
+
<img src="docs/images/0.4.18/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
|
|
72
|
+
<img src="docs/images/0.4.18/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
|
|
73
|
+
<img src="docs/images/0.4.18/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
|
|
74
|
+
<img src="docs/images/0.4.18/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
|
|
75
75
|
</p>
|
|
76
76
|
|
|
77
77
|
## Get started
|
package/docs/distributions.md
CHANGED
|
@@ -368,6 +368,39 @@ The ☆ star on Workspaces rows, Services and Graph cards marks a
|
|
|
368
368
|
favourite. It is one list, saved in `shell.pins`, so a star set on one
|
|
369
369
|
page shows on all of them.
|
|
370
370
|
|
|
371
|
+
### The service panel
|
|
372
|
+
|
|
373
|
+
A pinned service in the rail has **↗** at the end of its row, always
|
|
374
|
+
shown: it opens the service's URL in a new tab ("Visit <service> (opens a
|
|
375
|
+
new tab)", the URL as its tooltip). It is greyed out, saying "Not
|
|
376
|
+
running", while the service is down, and absent when the service has no
|
|
377
|
+
URL.
|
|
378
|
+
|
|
379
|
+
A click on the service's name (or Enter on its row, or its dot when the
|
|
380
|
+
rail is collapsed) opens its **panel** beside the row, without leaving
|
|
381
|
+
the page. ⌘-click, Ctrl-click or a middle click still opens Services in a
|
|
382
|
+
new tab. Esc or a click outside closes it, and focus goes back to the
|
|
383
|
+
row. Under 640 px wide it is a sheet at the bottom of the screen. The
|
|
384
|
+
Services card's name and the Graph card's **⋯** open the same panel.
|
|
385
|
+
|
|
386
|
+
Top to bottom, it has:
|
|
387
|
+
|
|
388
|
+
- the name, workspace, health dot, state ("up 2h", "down: HTTP 502",
|
|
389
|
+
"starting…") and URL;
|
|
390
|
+
- **Visit ↗**, then **Start** or **Stop**, **Restart** and **Logs**. These
|
|
391
|
+
call what the Services page calls (`/api/service/exec` with `up`,
|
|
392
|
+
`down` or `restart`, and `/api/service/logs`), each in a tab of the runs
|
|
393
|
+
dock. Logs opens the dock on that service;
|
|
394
|
+
- **Last test run** (the server's run history, `GET /api/runs/last`) and
|
|
395
|
+
**Run tests**, which runs the repo's suites (`/api/run?pillar=suites`);
|
|
396
|
+
- **Agents**: the agents working in the repo, each with **Open**, and
|
|
397
|
+
**+ Start agent here**, which opens the Start dialog with the workspace
|
|
398
|
+
and the repo's folder filled in;
|
|
399
|
+
- **Open in Services**, **Open in Graph** and **Unpin** (or **Pin**).
|
|
400
|
+
|
|
401
|
+
It stays current while it is open, from the same feeds the pages use. Delete
|
|
402
|
+
on a pinned row still unpins it.
|
|
403
|
+
|
|
371
404
|
## Health checks
|
|
372
405
|
|
|
373
406
|
A service declares its health per environment under `health`, in one of
|
|
@@ -2398,6 +2431,11 @@ started. The workspace header counts them ("2 agents").
|
|
|
2398
2431
|
`agents.runtimes.<id>.bin` to its path in <your user.yml>, or run
|
|
2399
2432
|
`<cli> agents doctor`" (see [Runtimes](#runtimes)). Nothing fails only
|
|
2400
2433
|
after Start.
|
|
2434
|
+
- **Folder** (optional) is where the agent runs: a folder inside the
|
|
2435
|
+
workspace's own, or a repo's folder of the workspace (relative to the
|
|
2436
|
+
workspace's folder, or absolute). Empty, it runs in the workspace's
|
|
2437
|
+
folder. A folder outside the workspace is refused (403). The service
|
|
2438
|
+
panel's **Start agent here** fills it with the repo's folder.
|
|
2401
2439
|
- The page and the CLI call the same operations and read the same agent
|
|
2402
2440
|
records, so an agent started in one is listed in the other. A workspace
|
|
2403
2441
|
lists its own agents only.
|
|
@@ -2412,7 +2450,7 @@ started. The workspace header counts them ("2 agents").
|
|
|
2412
2450
|
|
|
2413
2451
|
The routes: `GET /api/workspaces/<ws>/agents`, `POST
|
|
2414
2452
|
/api/workspaces/<ws>/agents` (`{ persona, runtime?, model?, version?,
|
|
2415
|
-
permission?, allow?, instruction?, id? }`, see
|
|
2453
|
+
permission?, allow?, instruction?, id?, folder? }`, see
|
|
2416
2454
|
[The Start dialog](#the-start-dialog)), `POST
|
|
2417
2455
|
/api/workspaces/<ws>/agents/<id>/stop` and `…/resume`, `POST
|
|
2418
2456
|
/api/workspaces/<ws>/sessions` (a plain session), `GET /api/personas`
|
package/package.json
CHANGED
package/src/ui/cli/agent-ops.ts
CHANGED
|
@@ -1409,6 +1409,12 @@ export interface StartRequest {
|
|
|
1409
1409
|
allow?: unknown;
|
|
1410
1410
|
/** AG-10.10: a display name for this agent (AG-4b's), to tell two apart. */
|
|
1411
1411
|
name?: unknown;
|
|
1412
|
+
/**
|
|
1413
|
+
* 0.4.18, Start agent here: the folder it runs in, the workspace's own
|
|
1414
|
+
* or one inside it (a repo's), relative to the workspace's folder or
|
|
1415
|
+
* absolute. Default: the workspace's folder.
|
|
1416
|
+
*/
|
|
1417
|
+
folder?: string;
|
|
1412
1418
|
/**
|
|
1413
1419
|
* The page's start (AG-10.1): a model must be one the runtime offers,
|
|
1414
1420
|
* never a free string (the CLI's --model for generic stays free).
|
|
@@ -1524,6 +1530,39 @@ interface StartMore {
|
|
|
1524
1530
|
bringIn?: { token: string; cwd: string };
|
|
1525
1531
|
}
|
|
1526
1532
|
|
|
1533
|
+
/**
|
|
1534
|
+
* 0.4.18: the folder a start runs in. Relative to the workspace's folder,
|
|
1535
|
+
* or absolute; it must be a directory inside the workspace's scope (its
|
|
1536
|
+
* folder, or a by-path repo's), never outside it.
|
|
1537
|
+
*/
|
|
1538
|
+
export function startFolder(
|
|
1539
|
+
d: AgentsResolved,
|
|
1540
|
+
ws: string,
|
|
1541
|
+
folder: string,
|
|
1542
|
+
): string {
|
|
1543
|
+
const where = "agents start";
|
|
1544
|
+
if (typeof folder !== "string" || !folder.trim() || folder.length > 4096)
|
|
1545
|
+
throw usage(`${where}: folder must be a path`);
|
|
1546
|
+
const dir = path.resolve(d.workspaceDir(ws), folder.trim());
|
|
1547
|
+
let real: string;
|
|
1548
|
+
try {
|
|
1549
|
+
real = fs.realpathSync(dir);
|
|
1550
|
+
} catch {
|
|
1551
|
+
throw usage(`${where}: folder ${dir} does not exist`, 404);
|
|
1552
|
+
}
|
|
1553
|
+
if (!fs.statSync(real).isDirectory())
|
|
1554
|
+
throw usage(`${where}: folder ${dir} is not a directory`);
|
|
1555
|
+
// The real folder only: a link inside the workspace that points out of
|
|
1556
|
+
// it is outside it (the scope's own folders are compared real, too).
|
|
1557
|
+
if (!inScope(real, d.workspaceFolders(ws)))
|
|
1558
|
+
throw refused(
|
|
1559
|
+
`${where}: folder ${dir} is not inside workspace ${ws}`,
|
|
1560
|
+
[],
|
|
1561
|
+
403,
|
|
1562
|
+
);
|
|
1563
|
+
return real;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1527
1566
|
export async function startAgent(
|
|
1528
1567
|
d: AgentsResolved,
|
|
1529
1568
|
req: StartRequest,
|
|
@@ -1571,6 +1610,10 @@ export async function startAgent(
|
|
|
1571
1610
|
...(rt.permissions ? { allow } : {}),
|
|
1572
1611
|
}),
|
|
1573
1612
|
...(more.firstPrompt ? { firstPrompt: more.firstPrompt } : {}),
|
|
1613
|
+
// 0.4.18: Start agent here: in a folder of the workspace's.
|
|
1614
|
+
...(req.folder !== undefined && !more.bringIn
|
|
1615
|
+
? { worktree: startFolder(d, ws, req.folder) }
|
|
1616
|
+
: {}),
|
|
1574
1617
|
// AG-17.1: the runtime's resume, in the folder the conversation ran in.
|
|
1575
1618
|
...(more.bringIn
|
|
1576
1619
|
? { worktree: more.bringIn.cwd, resumeToken: more.bringIn.token }
|
|
@@ -315,6 +315,8 @@ interface StartBody {
|
|
|
315
315
|
instruction?: unknown;
|
|
316
316
|
/** AG-10.10: a display name, checked as a rename's is. */
|
|
317
317
|
name?: unknown;
|
|
318
|
+
/** 0.4.18: the folder it runs in (Start agent here). */
|
|
319
|
+
folder?: string;
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
const START_KEYS = [
|
|
@@ -327,6 +329,7 @@ const START_KEYS = [
|
|
|
327
329
|
"allow",
|
|
328
330
|
"instruction",
|
|
329
331
|
"name",
|
|
332
|
+
"folder",
|
|
330
333
|
];
|
|
331
334
|
|
|
332
335
|
async function startBody(req: http.IncomingMessage): Promise<StartBody> {
|
|
@@ -374,6 +377,11 @@ async function startBody(req: http.IncomingMessage): Promise<StartBody> {
|
|
|
374
377
|
if (o.instruction !== undefined) out.instruction = o.instruction;
|
|
375
378
|
if (o.name !== undefined && o.name !== null && o.name !== "")
|
|
376
379
|
out.name = o.name;
|
|
380
|
+
if (o.folder !== undefined && o.folder !== null && o.folder !== "") {
|
|
381
|
+
if (typeof o.folder !== "string" || o.folder.length > 4096)
|
|
382
|
+
throw new AgentError("folder must be a path", { usage: true });
|
|
383
|
+
out.folder = o.folder;
|
|
384
|
+
}
|
|
377
385
|
return out as StartBody;
|
|
378
386
|
}
|
|
379
387
|
|
|
@@ -585,6 +593,7 @@ export async function handleAgentRoutes(
|
|
|
585
593
|
: {}),
|
|
586
594
|
...(body.allow !== undefined ? { allow: body.allow } : {}),
|
|
587
595
|
...(body.name !== undefined ? { name: body.name } : {}),
|
|
596
|
+
...(body.folder !== undefined ? { folder: body.folder } : {}),
|
|
588
597
|
strict: true,
|
|
589
598
|
});
|
|
590
599
|
const ov = s.rec.overrides?.length
|
|
@@ -203,6 +203,7 @@ function writeUrl() {
|
|
|
203
203
|
else u.searchParams.delete("open");
|
|
204
204
|
u.searchParams.set("layout", st.layout);
|
|
205
205
|
u.searchParams.delete("start");
|
|
206
|
+
u.searchParams.delete("folder");
|
|
206
207
|
const s = u.searchParams.toString().replace(/%2C/gi, ",");
|
|
207
208
|
history.replaceState(history.state, "", u.pathname + (s ? `?${s}` : ""));
|
|
208
209
|
paintSub();
|
|
@@ -2101,8 +2102,16 @@ function dialogMode(bring, change = null) {
|
|
|
2101
2102
|
);
|
|
2102
2103
|
$d("#ag-go").textContent = goLabel();
|
|
2103
2104
|
$d("#ag-conv-f").hidden = !bring;
|
|
2105
|
+
// 0.4.18: a new agent's folder (Start agent here fills it).
|
|
2106
|
+
for (const id of ["#ag-folder-l", "#ag-folder-h"]) {
|
|
2107
|
+
const el = $d(id);
|
|
2108
|
+
if (el) el.hidden = !!(bring || change);
|
|
2109
|
+
}
|
|
2104
2110
|
}
|
|
2105
2111
|
|
|
2112
|
+
/** 0.4.18: Start agent here (the service panel): ?start=1&ws=…&folder=…. */
|
|
2113
|
+
let startPrefill = null;
|
|
2114
|
+
|
|
2106
2115
|
/** The submit button's words for the dialog's mode. */
|
|
2107
2116
|
function goLabel() {
|
|
2108
2117
|
return t(sd.change ? "ag.pc.go" : sd.bring ? "ag.bring.go" : "ag.go");
|
|
@@ -2124,12 +2133,18 @@ async function openStart(bring = false, change = null) {
|
|
|
2124
2133
|
const dlg = $d("#ag-dlg");
|
|
2125
2134
|
if (!dlg || dlg.open) return;
|
|
2126
2135
|
dialogMode(bring, change);
|
|
2136
|
+
const pre = change || bring ? null : startPrefill;
|
|
2137
|
+
startPrefill = null;
|
|
2127
2138
|
const shown = change ? [change.workspace] : viewWorkspaces();
|
|
2139
|
+
if (pre?.ws && !shown.includes(pre.ws)) shown.push(pre.ws);
|
|
2128
2140
|
const wsSel = $d("#ag-ws");
|
|
2129
2141
|
wsSel.innerHTML = shown
|
|
2130
2142
|
.map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
|
|
2131
2143
|
.join("");
|
|
2132
2144
|
wsSel.disabled = shown.length <= 1;
|
|
2145
|
+
if (pre?.ws) wsSel.value = pre.ws;
|
|
2146
|
+
const folder = $d("#ag-folder");
|
|
2147
|
+
if (folder) folder.value = pre?.folder ?? "";
|
|
2133
2148
|
sd.f = { q: "", cat: "", src: "" };
|
|
2134
2149
|
sd.det = false;
|
|
2135
2150
|
sd.what = false;
|
|
@@ -2170,6 +2185,8 @@ function startBody() {
|
|
|
2170
2185
|
if (task) body.instruction = task;
|
|
2171
2186
|
const name = $d("#ag-name").value.trim();
|
|
2172
2187
|
if (name) body.name = name;
|
|
2188
|
+
const folder = $d("#ag-folder")?.value.trim();
|
|
2189
|
+
if (folder) body.folder = folder;
|
|
2173
2190
|
return body;
|
|
2174
2191
|
}
|
|
2175
2192
|
|
|
@@ -2628,6 +2645,8 @@ function translateStatic(root) {
|
|
|
2628
2645
|
set("#ag-what-p", "pe.what");
|
|
2629
2646
|
set("#ag-task-t", "ag.new.task");
|
|
2630
2647
|
set("#ag-name-t", "ag.new.name");
|
|
2648
|
+
set("#ag-folder-t", "ag.new.folder");
|
|
2649
|
+
set("#ag-folder-h", "ag.new.folderHelp");
|
|
2631
2650
|
set("#ag-adv-s", "ag.new.adv");
|
|
2632
2651
|
set("#ag-note", "ag.new.note");
|
|
2633
2652
|
const task = root.querySelector("#ag-task");
|
|
@@ -2659,6 +2678,10 @@ export async function mountAgentsPage(root) {
|
|
|
2659
2678
|
const wanted = openFromUrl(location.search);
|
|
2660
2679
|
const linked = layoutFromUrl(location.search);
|
|
2661
2680
|
const startAsked = new URLSearchParams(location.search).get("start") === "1";
|
|
2681
|
+
if (startAsked) {
|
|
2682
|
+
const q = new URLSearchParams(location.search);
|
|
2683
|
+
startPrefill = { ws: q.get("ws") || "", folder: q.get("folder") || "" };
|
|
2684
|
+
}
|
|
2662
2685
|
|
|
2663
2686
|
root.querySelector("#agl").addEventListener("click", (e) => {
|
|
2664
2687
|
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>
|
|
@@ -349,6 +349,28 @@
|
|
|
349
349
|
color: var(--accent-text);
|
|
350
350
|
background: var(--sunken);
|
|
351
351
|
}
|
|
352
|
+
/* 0.4.18: ⋯ opens the service panel (svc-panel.js), as a pinned row does.
|
|
353
|
+
It sits before the star, which stays top-right. */
|
|
354
|
+
.dg-svp {
|
|
355
|
+
margin-left: auto;
|
|
356
|
+
flex: none;
|
|
357
|
+
background: none;
|
|
358
|
+
border: 1px solid transparent;
|
|
359
|
+
border-radius: 6px;
|
|
360
|
+
color: var(--faint);
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
font-size: 13px;
|
|
363
|
+
line-height: 1;
|
|
364
|
+
padding: 1px 4px;
|
|
365
|
+
}
|
|
366
|
+
.dg-svp + .dg-star {
|
|
367
|
+
margin-left: 0;
|
|
368
|
+
}
|
|
369
|
+
.dg-svp:hover,
|
|
370
|
+
.dg-svp[aria-expanded="true"] {
|
|
371
|
+
color: var(--accent-text);
|
|
372
|
+
background: var(--sunken);
|
|
373
|
+
}
|
|
352
374
|
.dg-dot {
|
|
353
375
|
display: inline-block;
|
|
354
376
|
width: 9px;
|
|
@@ -256,9 +256,26 @@ 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
|
+
function focusFromHash() {
|
|
266
|
+
if (hashFocused || !location.hash.startsWith("#focus=")) return;
|
|
267
|
+
let id = "";
|
|
268
|
+
try {
|
|
269
|
+
id = decodeURIComponent(location.hash.slice(7));
|
|
270
|
+
} catch {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (M.byId.has(id)) focusOn({ id });
|
|
274
|
+
else if (M.byRepo.has(id)) focusOn({ repo: id, viaRepo: true });
|
|
275
|
+
else return;
|
|
276
|
+
hashFocused = true;
|
|
277
|
+
}
|
|
278
|
+
|
|
262
279
|
/** A /api/health update: refresh dots, health lines and the open panel in place. */
|
|
263
280
|
export function graphHealth(results) {
|
|
264
281
|
if (!M || !host?.isConnected) return;
|
|
@@ -511,6 +528,13 @@ function starBtn(id) {
|
|
|
511
528
|
: `${on ? "Unpin" : "Pin"} ${id}`;
|
|
512
529
|
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
530
|
}
|
|
531
|
+
/** 0.4.18: ⋯ opens the service's quick panel (svc-panel.js), the rail's. */
|
|
532
|
+
function svpBtn(id) {
|
|
533
|
+
const sh = window.denShell;
|
|
534
|
+
if (!sh?.openServicePanel) return "";
|
|
535
|
+
const label = sh.t ? sh.t("svp.more", { s: id }) : id;
|
|
536
|
+
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>`;
|
|
537
|
+
}
|
|
514
538
|
/** A card's actions: the first three, and Visit ↗ always when it has a URL. */
|
|
515
539
|
function cardActs(s) {
|
|
516
540
|
const acts = actionsFor(s);
|
|
@@ -522,7 +546,7 @@ function cardActs(s) {
|
|
|
522
546
|
}
|
|
523
547
|
function cardInner(s) {
|
|
524
548
|
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>
|
|
549
|
+
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
550
|
<div class="r2" title="${esc(s.id)}${s.inWorkspaces.length > 1 ? `: in ${esc(s.inWorkspaces.join(", "))}` : ""}">${esc(s.repo + ownersLabel(s.inWorkspaces))}</div>
|
|
527
551
|
<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
552
|
<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.
|
|
@@ -55,6 +55,41 @@
|
|
|
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
|
+
"svp.close": "Cerrar",
|
|
90
|
+
"ag.new.folder": "Carpeta (opcional)",
|
|
91
|
+
"ag.new.folderHelp":
|
|
92
|
+
"Dentro de la carpeta del espacio de trabajo. Vacío: la suya.",
|
|
58
93
|
"act.pinX": "Fijar {s}",
|
|
59
94
|
"act.unpinX": "Quitar {s}",
|
|
60
95
|
"toast.pinned": "{s} fijado",
|
|
@@ -52,6 +52,41 @@
|
|
|
52
52
|
"act.unpin": "ピン留めを外す",
|
|
53
53
|
"act.visit": "開く",
|
|
54
54
|
"act.visitX": "{s} を開く(新しいタブ)",
|
|
55
|
+
// 0.4.18: the service panel (svc-panel.js) and Start's Folder.
|
|
56
|
+
"svp.label": "{s}: クイックパネル",
|
|
57
|
+
"svp.more": "{s} のクイックパネル",
|
|
58
|
+
"svp.notRunning": "停止中",
|
|
59
|
+
"svp.up": "稼働 {d}",
|
|
60
|
+
"svp.upNow": "稼働中",
|
|
61
|
+
"svp.down": "停止: {c}",
|
|
62
|
+
"svp.downNone": "停止",
|
|
63
|
+
"svp.starting": "起動中…",
|
|
64
|
+
"svp.stopped": "停止",
|
|
65
|
+
"svp.noUrl": "URL なし",
|
|
66
|
+
"svp.start": "開始",
|
|
67
|
+
"svp.stop": "停止",
|
|
68
|
+
"svp.restart": "再起動",
|
|
69
|
+
"svp.logs": "ログ",
|
|
70
|
+
"svp.stopping": "{s} を停止中",
|
|
71
|
+
"svp.tests": "前回のテスト",
|
|
72
|
+
"svp.testsOk": "✓ {t}",
|
|
73
|
+
"svp.testsBad": "✗ 失敗 {t}",
|
|
74
|
+
"svp.testsRun": "実行中…",
|
|
75
|
+
"svp.testsNone": "—",
|
|
76
|
+
"svp.runTests": "テストを実行",
|
|
77
|
+
"svp.testsDone": "テスト成功: {s}",
|
|
78
|
+
"svp.testsFailed": "テスト失敗: {s}",
|
|
79
|
+
"svp.agents": "エージェント",
|
|
80
|
+
"svp.noAgents": "このリポジトリにエージェントはいません。",
|
|
81
|
+
"svp.open": "開く",
|
|
82
|
+
"svp.openAg": "{s} を開く",
|
|
83
|
+
"svp.startHere": "ここでエージェントを開始",
|
|
84
|
+
"svp.openSvc": "サービスで開く",
|
|
85
|
+
"svp.openGraph": "グラフで開く",
|
|
86
|
+
"svp.close": "閉じる",
|
|
87
|
+
"ag.new.folder": "フォルダー(任意)",
|
|
88
|
+
"ag.new.folderHelp":
|
|
89
|
+
"ワークスペースのフォルダー内。空欄ならワークスペースのフォルダー。",
|
|
55
90
|
"act.pinX": "{s} をピン留め",
|
|
56
91
|
"act.unpinX": "{s} のピン留めを外す",
|
|
57
92
|
"toast.pinned": "{s} をピン留めしました",
|
|
@@ -1874,7 +1874,12 @@
|
|
|
1874
1874
|
if (updateUrl) {
|
|
1875
1875
|
const params = new URLSearchParams(location.search);
|
|
1876
1876
|
params.set("ws", name);
|
|
1877
|
-
|
|
1877
|
+
// The hash stays (#focus=<id>: Open in Graph's card).
|
|
1878
|
+
history.replaceState(
|
|
1879
|
+
null,
|
|
1880
|
+
"",
|
|
1881
|
+
"?" + params.toString() + location.hash,
|
|
1882
|
+
);
|
|
1878
1883
|
}
|
|
1879
1884
|
document
|
|
1880
1885
|
.querySelectorAll(".ws-item")
|
|
@@ -4273,7 +4278,11 @@
|
|
|
4273
4278
|
const params = new URLSearchParams(location.search);
|
|
4274
4279
|
params.delete("ws");
|
|
4275
4280
|
const qs = params.toString();
|
|
4276
|
-
history.replaceState(
|
|
4281
|
+
history.replaceState(
|
|
4282
|
+
null,
|
|
4283
|
+
"",
|
|
4284
|
+
(qs ? "?" + qs : location.pathname) + location.hash,
|
|
4285
|
+
);
|
|
4277
4286
|
}
|
|
4278
4287
|
await loadWorkspaces();
|
|
4279
4288
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a service's live log comes from (0.4.18): one rule for the Logs
|
|
3
|
+
* page and the service panel's Logs, so they never disagree.
|
|
4
|
+
*
|
|
5
|
+
* - Den's own server log (the "__DEN_CLI__/server" pseudo-service):
|
|
6
|
+
* /api/server-log, the server's log ring;
|
|
7
|
+
* - a local service: /api/service/taillog, the workspace page's source
|
|
8
|
+
* chain (the manifest's command, the conventional container, the
|
|
9
|
+
* daemon's startup log). /api/service/logs answers 404 for a service
|
|
10
|
+
* with no `logs:` block;
|
|
11
|
+
* - any other env: /api/service/logs, for cloud sources and the reauth
|
|
12
|
+
* flow.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The server's own log, shown like a service's. */
|
|
16
|
+
export function isServerLog(repo, service) {
|
|
17
|
+
return repo === "__DEN_CLI__" && service === "server";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The SSE URL of `repo/service`'s live log in `env`. */
|
|
21
|
+
export function logStreamUrl(repo, service, env) {
|
|
22
|
+
if (isServerLog(repo, service)) return "/api/server-log";
|
|
23
|
+
if (env === "local")
|
|
24
|
+
return `/api/service/taillog?${new URLSearchParams({ repo, service })}`;
|
|
25
|
+
return `/api/service/logs?${new URLSearchParams({ repo, service, env })}`;
|
|
26
|
+
}
|
|
@@ -476,6 +476,7 @@
|
|
|
476
476
|
envReady,
|
|
477
477
|
initWorkspacePill,
|
|
478
478
|
} from "/header.js";
|
|
479
|
+
import { isServerLog, logStreamUrl } from "/log-source.js";
|
|
479
480
|
|
|
480
481
|
// ── Safe highlight rendering ──────────────────────────────────────────────
|
|
481
482
|
// Log lines are UNTRUSTED (any service can log attacker-influenced text,
|
|
@@ -550,11 +551,9 @@
|
|
|
550
551
|
const logStreams = new Map();
|
|
551
552
|
|
|
552
553
|
// "__DEN_CLI__/server" is a pseudo-service that taps the server's own log ring
|
|
553
|
-
// via /api/server-log instead of /api/service/logs
|
|
554
|
-
// page treats it like a normal
|
|
555
|
-
|
|
556
|
-
return repo === "__DEN_CLI__" && service === "server";
|
|
557
|
-
}
|
|
554
|
+
// via /api/server-log instead of /api/service/logs (isServerLog,
|
|
555
|
+
// log-source.js). Everything else in the page treats it like a normal
|
|
556
|
+
// log stream.
|
|
558
557
|
function formatServerLogEntry(entry) {
|
|
559
558
|
// "14:07:41 [lifecycle] ▶ run synthetic/local · modules=all [abcxyz]"
|
|
560
559
|
// An agent's action reads "[agent]" and ends "· by agent <client> …".
|
|
@@ -1075,21 +1074,9 @@
|
|
|
1075
1074
|
// ─────────────────────────────────────────────────────────────────────
|
|
1076
1075
|
|
|
1077
1076
|
const serverLog = isServerLog(repo, service);
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1081
|
-
// /api/service/logs 404s for services with no logs block (shop-web,
|
|
1082
|
-
// control-plane). Non-local envs keep /api/service/logs for
|
|
1083
|
-
// cloudwatch sources + the AWS reauth flow.
|
|
1084
|
-
const es = serverLog
|
|
1085
|
-
? new EventSource("/api/server-log")
|
|
1086
|
-
: env === "local"
|
|
1087
|
-
? new EventSource(
|
|
1088
|
-
`/api/service/taillog?${new URLSearchParams({ repo, service })}`,
|
|
1089
|
-
)
|
|
1090
|
-
: new EventSource(
|
|
1091
|
-
`/api/service/logs?${new URLSearchParams({ repo, service, env })}`,
|
|
1092
|
-
);
|
|
1077
|
+
// Where the stream comes from (taillog for local, logs elsewhere):
|
|
1078
|
+
// log-source.js, which the service panel's Logs uses too.
|
|
1079
|
+
const es = new EventSource(logStreamUrl(repo, service, env));
|
|
1093
1080
|
|
|
1094
1081
|
es.onmessage = (e) => {
|
|
1095
1082
|
const msg = JSON.parse(e.data);
|