@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
|
@@ -52,6 +52,43 @@
|
|
|
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
|
+
"graph.outside": "{s} は {ws} にあり、この表示の範囲外です。",
|
|
87
|
+
"graph.showWs": "{ws} を表示",
|
|
88
|
+
"svp.close": "閉じる",
|
|
89
|
+
"ag.new.folder": "フォルダー(任意)",
|
|
90
|
+
"ag.new.folderHelp":
|
|
91
|
+
"ワークスペースのフォルダー内。空欄ならワークスペースのフォルダー。",
|
|
55
92
|
"act.pinX": "{s} をピン留め",
|
|
56
93
|
"act.unpinX": "{s} のピン留めを外す",
|
|
57
94
|
"toast.pinned": "{s} をピン留めしました",
|
|
@@ -267,6 +304,10 @@
|
|
|
267
304
|
"err.signin.step2":
|
|
268
305
|
"承認されると、このページは自動でサインインします。コードの有効期限は 10 分です。",
|
|
269
306
|
"err.signin.waiting": "承認を待っています…",
|
|
307
|
+
"err.signin.windowsSteps":
|
|
308
|
+
"Windows ターミナルか PowerShell を開き、これを貼り付けて Enter キーを押します。",
|
|
309
|
+
"err.signin.nothingOpened":
|
|
310
|
+
"何も開きませんでしたか?下のコマンドをコピーして、ターミナルアプリに貼り付けてください。",
|
|
270
311
|
"err.signin.noTerminal":
|
|
271
312
|
"{host} のターミナルがありませんか?このコンピューターで次を実行します:",
|
|
272
313
|
"err.signin.openTerminal": "ターミナルを開く",
|
|
@@ -531,6 +572,8 @@
|
|
|
531
572
|
"ap.do.repo_add.new":
|
|
532
573
|
"{path} のリポジトリを新しいワークスペース「{workspace}」に追加",
|
|
533
574
|
"ap.do.service_try": "{service} を起動",
|
|
575
|
+
"ap.do.service_try.activates":
|
|
576
|
+
"{service} を起動 ({workspace} を有効にします)",
|
|
534
577
|
"ap.says": "エージェントのコメント:",
|
|
535
578
|
"ap.review": "確認",
|
|
536
579
|
"ap.hide": "詳細を隠す",
|
|
@@ -672,6 +715,37 @@
|
|
|
672
715
|
"ag.ws": "ワークスペース",
|
|
673
716
|
"ag.pal": "エージェント › {ws} / {name}",
|
|
674
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": "保存しました。次回の起動から使われます。",
|
|
675
749
|
"ag.cap":
|
|
676
750
|
"ペインは最大 4 つです。{id} のペインを閉じました(実行は続きます)",
|
|
677
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,
|
|
@@ -1874,7 +1880,12 @@
|
|
|
1874
1880
|
if (updateUrl) {
|
|
1875
1881
|
const params = new URLSearchParams(location.search);
|
|
1876
1882
|
params.set("ws", name);
|
|
1877
|
-
|
|
1883
|
+
// The hash stays (#focus=<id>: Open in Graph's card).
|
|
1884
|
+
history.replaceState(
|
|
1885
|
+
null,
|
|
1886
|
+
"",
|
|
1887
|
+
"?" + params.toString() + location.hash,
|
|
1888
|
+
);
|
|
1878
1889
|
}
|
|
1879
1890
|
document
|
|
1880
1891
|
.querySelectorAll(".ws-item")
|
|
@@ -4273,7 +4284,11 @@
|
|
|
4273
4284
|
const params = new URLSearchParams(location.search);
|
|
4274
4285
|
params.delete("ws");
|
|
4275
4286
|
const qs = params.toString();
|
|
4276
|
-
history.replaceState(
|
|
4287
|
+
history.replaceState(
|
|
4288
|
+
null,
|
|
4289
|
+
"",
|
|
4290
|
+
(qs ? "?" + qs : location.pathname) + location.hash,
|
|
4291
|
+
);
|
|
4277
4292
|
}
|
|
4278
4293
|
await loadWorkspaces();
|
|
4279
4294
|
}
|
|
@@ -4644,6 +4659,19 @@
|
|
|
4644
4659
|
});
|
|
4645
4660
|
|
|
4646
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),
|
|
4647
4675
|
maximize: {
|
|
4648
4676
|
isOn: () => graphMax(),
|
|
4649
4677
|
toggle: () => setGraphMax(!graphMax()),
|
|
@@ -4692,6 +4720,8 @@
|
|
|
4692
4720
|
if (!services || services.length === 0) {
|
|
4693
4721
|
wrap.innerHTML =
|
|
4694
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);
|
|
4695
4725
|
return;
|
|
4696
4726
|
}
|
|
4697
4727
|
mountGraph(
|
|
@@ -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);
|
|
@@ -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;
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server agent's page (ON-6): this Den's own agent in a terminal pane,
|
|
3
|
+
* framed as the page, with "Open in Agents" as the way out. A fresh Den's
|
|
4
|
+
* first page (the server serves it at /). Its status is GET
|
|
5
|
+
* /api/server-agent (server-agent-routes.ts).
|
|
6
|
+
*
|
|
7
|
+
* - Fresh, with a runtime that can run it: it starts by itself. It only
|
|
8
|
+
* reads until the person allows a write (each write is held for the
|
|
9
|
+
* browser's Allow, ON-3).
|
|
10
|
+
* - No runtime: the manual path, and how to install a runtime.
|
|
11
|
+
* - Running on Claude on a fresh Den: a line saying Claude asks to trust
|
|
12
|
+
* its folder (choose Yes), and one saying to log in to Claude in the
|
|
13
|
+
* terminal if it asks (Den can't tell whether it is logged in). Which
|
|
14
|
+
* runtime it runs on is its record's (runningRuntime), never the
|
|
15
|
+
* picker's: the two differ after the picker changes, or when the runtime
|
|
16
|
+
* is not found again on a later read.
|
|
17
|
+
* - Started by itself, but not running yet when the page reads it back:
|
|
18
|
+
* the page reads again, a few times, until it runs (so the terminal and
|
|
19
|
+
* its lines come up without a reload).
|
|
20
|
+
* - Not fresh and not running: a Start button (never started by itself).
|
|
21
|
+
* - The runtime picker saves the person's choice (agents.server_runtime),
|
|
22
|
+
* and the model picker beside it (agents.server_model), with only the
|
|
23
|
+
* runtime's own models.
|
|
24
|
+
*
|
|
25
|
+
* serverAgentView(status) is pure (HTML from the status JSON), so tests
|
|
26
|
+
* need no server.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const t = (key, vars) => globalThis.window?.denShell?.t?.(key, vars) ?? key;
|
|
30
|
+
const esc = (s) =>
|
|
31
|
+
String(s ?? "").replace(
|
|
32
|
+
/[&<>"']/g,
|
|
33
|
+
(c) =>
|
|
34
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
35
|
+
c
|
|
36
|
+
],
|
|
37
|
+
);
|
|
38
|
+
const CLI = "__DEN_CLI__";
|
|
39
|
+
|
|
40
|
+
const RUNNING = new Set(["running", "stopping"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What the page shows for `status`: its header line, its body, whether
|
|
44
|
+
* the agent starts by itself, and whether a terminal opens.
|
|
45
|
+
*
|
|
46
|
+
* { sub, body, autoStart, terminal }
|
|
47
|
+
*/
|
|
48
|
+
export function serverAgentView(s) {
|
|
49
|
+
const running = RUNNING.has(s.state) && !!s.sessionId;
|
|
50
|
+
const hasRuntime = !!s.runtime;
|
|
51
|
+
const sub = s.fresh ? t("sa.sub.fresh") : t("sa.sub");
|
|
52
|
+
const readsOnly = `<p class="note sa-reads" data-note="reads-only">${esc(t("sa.readsOnly"))}</p>`;
|
|
53
|
+
// Den can't tell whether Claude is logged in (ON-6). On a fresh Den's
|
|
54
|
+
// first run, one line above the terminal says a login prompt is
|
|
55
|
+
// expected, so Claude's own login screen never reads as a crash.
|
|
56
|
+
// Claude Code asks once whether to trust the agent's folder, which Den
|
|
57
|
+
// made for it; Den doesn't answer for the person (that is Claude's own
|
|
58
|
+
// file, ~/.claude.json, with no documented way to pre-accept).
|
|
59
|
+
const onClaude =
|
|
60
|
+
s.fresh &&
|
|
61
|
+
(running ? (s.runningRuntime ?? s.runtime?.id) : s.runtime?.id) ===
|
|
62
|
+
"den-claude";
|
|
63
|
+
const login = onClaude
|
|
64
|
+
? `<p class="note sa-trust" data-note="trust">${esc(t("sa.trust"))}</p>` +
|
|
65
|
+
`<p class="note sa-login" data-note="login">${esc(t("sa.login", { name: s.name }))}</p>`
|
|
66
|
+
: "";
|
|
67
|
+
if (running)
|
|
68
|
+
return {
|
|
69
|
+
sub,
|
|
70
|
+
body: login + readsOnly,
|
|
71
|
+
autoStart: false,
|
|
72
|
+
terminal: s.sessionId,
|
|
73
|
+
};
|
|
74
|
+
if (!hasRuntime)
|
|
75
|
+
return {
|
|
76
|
+
sub,
|
|
77
|
+
body: noRuntimeHtml(s),
|
|
78
|
+
autoStart: false,
|
|
79
|
+
terminal: null,
|
|
80
|
+
};
|
|
81
|
+
// Fresh and never started (or stopped): it starts by itself.
|
|
82
|
+
if (s.fresh)
|
|
83
|
+
return {
|
|
84
|
+
sub,
|
|
85
|
+
body: `<p class="note" data-note="starting">${esc(t("sa.starting"))}</p>${readsOnly}`,
|
|
86
|
+
autoStart: true,
|
|
87
|
+
terminal: null,
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
sub,
|
|
91
|
+
body:
|
|
92
|
+
`<div class="sa-idle" data-note="idle"><p>${esc(t("sa.idle", { name: s.name }))}</p>` +
|
|
93
|
+
`<button type="button" class="den-btn primary" data-act="start">${esc(t("sa.start"))}</button></div>` +
|
|
94
|
+
readsOnly,
|
|
95
|
+
autoStart: false,
|
|
96
|
+
terminal: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** No runtime can run it: how to install one, and the manual path. */
|
|
101
|
+
function noRuntimeHtml(s) {
|
|
102
|
+
const installs = (s.runtimes ?? [])
|
|
103
|
+
.filter((r) => !r.usable)
|
|
104
|
+
.map(
|
|
105
|
+
(r) =>
|
|
106
|
+
`<li data-runtime="${esc(r.id)}"><b>${esc(r.label ?? r.id)}</b>` +
|
|
107
|
+
(r.available && !r.denTools
|
|
108
|
+
? ` · ${esc(t("sa.noTools"))}`
|
|
109
|
+
: r.install
|
|
110
|
+
? ` · <code>${esc(r.install)}</code>`
|
|
111
|
+
: "") +
|
|
112
|
+
`</li>`,
|
|
113
|
+
)
|
|
114
|
+
.join("");
|
|
115
|
+
const manual = (s.manual ?? [])
|
|
116
|
+
.map((c) => `<li><code>${esc(c)}</code></li>`)
|
|
117
|
+
.join("");
|
|
118
|
+
return (
|
|
119
|
+
`<section class="sa-none" data-note="no-runtime"><h2 class="sec">${esc(t("sa.none.h"))}</h2>` +
|
|
120
|
+
`<p>${esc(t("sa.none"))}</p>` +
|
|
121
|
+
(installs ? `<ul class="sa-install">${installs}</ul>` : "") +
|
|
122
|
+
`</section>` +
|
|
123
|
+
`<section class="sa-manual" data-note="manual"><h2 class="sec">${esc(t("sa.manual.h"))}</h2>` +
|
|
124
|
+
`<p>${esc(t("sa.manual"))}</p><ol class="sa-steps">${manual}</ol></section>`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The model picker's options: the runtime's own models, the chosen one selected. */
|
|
129
|
+
export function modelOptionsHtml(s) {
|
|
130
|
+
const models = s.models ?? [];
|
|
131
|
+
if (!models.length) return "";
|
|
132
|
+
return models
|
|
133
|
+
.map(
|
|
134
|
+
(m) =>
|
|
135
|
+
`<option value="${esc(m.id)}"${s.model === m.id ? " selected" : ""}>${esc(m.label ?? m.id)}</option>`,
|
|
136
|
+
)
|
|
137
|
+
.join("");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The runtime picker's options: the usable ones, the chosen one first. */
|
|
141
|
+
export function runtimeOptionsHtml(s) {
|
|
142
|
+
const usable = (s.runtimes ?? []).filter((r) => r.usable);
|
|
143
|
+
if (!usable.length) return "";
|
|
144
|
+
return usable
|
|
145
|
+
.map(
|
|
146
|
+
(r) =>
|
|
147
|
+
`<option value="${esc(r.id)}"${s.runtime?.id === r.id ? " selected" : ""}>${esc(r.label ?? r.id)}</option>`,
|
|
148
|
+
)
|
|
149
|
+
.join("");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── the page ────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const st = {
|
|
155
|
+
root: null,
|
|
156
|
+
status: null,
|
|
157
|
+
pane: null,
|
|
158
|
+
starting: false,
|
|
159
|
+
autoTried: false,
|
|
160
|
+
/** Reads after a start that came back not running yet. */
|
|
161
|
+
rereads: 0,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** How many times, a second apart, a start is read back until it runs. */
|
|
165
|
+
const REREADS = 15;
|
|
166
|
+
|
|
167
|
+
async function readStatus() {
|
|
168
|
+
try {
|
|
169
|
+
const r = await fetch("/api/server-agent", { headers: { accept: "json" } });
|
|
170
|
+
return r.ok ? await r.json() : null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function paint() {
|
|
177
|
+
const s = st.status;
|
|
178
|
+
const sub = st.root.querySelector("#sa-sub");
|
|
179
|
+
const body = st.root.querySelector("#sa-body");
|
|
180
|
+
const open = st.root.querySelector("#sa-open");
|
|
181
|
+
const pick = st.root.querySelector("#sa-runtime");
|
|
182
|
+
const mpick = st.root.querySelector("#sa-model");
|
|
183
|
+
body.removeAttribute("aria-busy");
|
|
184
|
+
if (!s) {
|
|
185
|
+
sub.textContent = t("sa.unread");
|
|
186
|
+
body.innerHTML = "";
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const v = serverAgentView(s);
|
|
190
|
+
sub.textContent = v.sub;
|
|
191
|
+
body.innerHTML = v.body;
|
|
192
|
+
const opts = runtimeOptionsHtml(s);
|
|
193
|
+
pick.hidden = !opts;
|
|
194
|
+
if (opts) pick.querySelector("select").innerHTML = opts;
|
|
195
|
+
const mopts = modelOptionsHtml(s);
|
|
196
|
+
if (mpick) {
|
|
197
|
+
mpick.hidden = !mopts;
|
|
198
|
+
if (mopts) mpick.querySelector("select").innerHTML = mopts;
|
|
199
|
+
}
|
|
200
|
+
open.hidden = !v.terminal;
|
|
201
|
+
if (v.terminal)
|
|
202
|
+
open.setAttribute("href", `/agents?open=${encodeURIComponent(v.terminal)}`);
|
|
203
|
+
return v;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function refresh() {
|
|
207
|
+
st.status = await readStatus();
|
|
208
|
+
const v = paint();
|
|
209
|
+
if (!v) return;
|
|
210
|
+
if (v.terminal) void openTerminal(v.terminal);
|
|
211
|
+
else closeTerminal();
|
|
212
|
+
// Once per page: a start that ends at once is never retried in a loop.
|
|
213
|
+
if (v.autoStart && !st.autoTried) {
|
|
214
|
+
st.autoTried = true;
|
|
215
|
+
void start();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Started, but not running yet: read again until it is.
|
|
219
|
+
if (st.autoTried && !v.terminal && v.autoStart && st.rereads < REREADS) {
|
|
220
|
+
st.rereads++;
|
|
221
|
+
setTimeout(() => void refresh(), 1000);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function start() {
|
|
226
|
+
if (st.starting) return;
|
|
227
|
+
st.starting = true;
|
|
228
|
+
try {
|
|
229
|
+
await window.denShell.api("POST", "/api/server-agent/start", {});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
st.starting = false;
|
|
232
|
+
const body = st.root.querySelector("#sa-body");
|
|
233
|
+
const why = err?.message ?? String(err);
|
|
234
|
+
body.innerHTML = `<p class="note bad" data-note="failed" role="alert">${esc(t("sa.failed", { why, cli: CLI }))}</p>`;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
st.starting = false;
|
|
238
|
+
await refresh();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function saveModel(id) {
|
|
242
|
+
try {
|
|
243
|
+
await window.denShell.api("POST", "/api/server-agent/model", {
|
|
244
|
+
model: id,
|
|
245
|
+
});
|
|
246
|
+
window.denShell?.toast?.(t("sa.saved"));
|
|
247
|
+
} catch (err) {
|
|
248
|
+
window.denShell?.errorToast?.(err);
|
|
249
|
+
}
|
|
250
|
+
await refresh();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function saveRuntime(id) {
|
|
254
|
+
try {
|
|
255
|
+
await window.denShell.api("POST", "/api/server-agent/runtime", {
|
|
256
|
+
runtime: id,
|
|
257
|
+
});
|
|
258
|
+
window.denShell?.toast?.(t("sa.saved"));
|
|
259
|
+
} catch (err) {
|
|
260
|
+
window.denShell?.errorToast?.(err);
|
|
261
|
+
}
|
|
262
|
+
await refresh();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── the terminal ────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
function cssVar(name) {
|
|
268
|
+
const v = getComputedStyle(document.documentElement)
|
|
269
|
+
.getPropertyValue(name)
|
|
270
|
+
.trim();
|
|
271
|
+
return v || undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function csrf() {
|
|
275
|
+
try {
|
|
276
|
+
const r = await fetch("/api/session", { credentials: "same-origin" });
|
|
277
|
+
const b = r.ok ? await r.json() : null;
|
|
278
|
+
return typeof b?.csrf === "string" ? b.csrf : null;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function closeTerminal() {
|
|
285
|
+
const p = st.pane;
|
|
286
|
+
if (!p) return;
|
|
287
|
+
st.pane = null;
|
|
288
|
+
p.closed = true;
|
|
289
|
+
p.sock?.close();
|
|
290
|
+
p.term?.dispose();
|
|
291
|
+
p.observer?.disconnect();
|
|
292
|
+
p.el.remove();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function openTerminal(id) {
|
|
296
|
+
if (st.pane?.id === id) return;
|
|
297
|
+
closeTerminal();
|
|
298
|
+
const el = document.createElement("div");
|
|
299
|
+
el.className = "sa-term";
|
|
300
|
+
el.dataset.id = id;
|
|
301
|
+
el.innerHTML = `<div class="tbox"></div>`;
|
|
302
|
+
st.root.querySelector("#sa-pane").appendChild(el);
|
|
303
|
+
const p = { id, el, term: null, fit: null, sock: null, closed: false };
|
|
304
|
+
st.pane = p;
|
|
305
|
+
let lib;
|
|
306
|
+
try {
|
|
307
|
+
const [x, f] = await Promise.all([
|
|
308
|
+
import("/vendor/xterm.mjs"),
|
|
309
|
+
import("/vendor/addon-fit.mjs"),
|
|
310
|
+
]);
|
|
311
|
+
lib = { Terminal: x.Terminal, FitAddon: f.FitAddon };
|
|
312
|
+
} catch (err) {
|
|
313
|
+
el.innerHTML = `<p class="note">${esc(err?.message ?? String(err))}</p>`;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (p.closed) return;
|
|
317
|
+
const bg = cssVar("--sunken");
|
|
318
|
+
const term = new lib.Terminal({
|
|
319
|
+
fontFamily: cssVar("--mono") ?? "monospace",
|
|
320
|
+
fontSize: 12,
|
|
321
|
+
lineHeight: 1.25,
|
|
322
|
+
scrollback: 5000,
|
|
323
|
+
theme: { background: bg, foreground: cssVar("--text"), cursorAccent: bg },
|
|
324
|
+
});
|
|
325
|
+
const fit = new lib.FitAddon();
|
|
326
|
+
term.loadAddon(fit);
|
|
327
|
+
term.open(el.querySelector(".tbox"));
|
|
328
|
+
p.term = term;
|
|
329
|
+
p.fit = fit;
|
|
330
|
+
const refit = () => {
|
|
331
|
+
try {
|
|
332
|
+
fit.fit();
|
|
333
|
+
} catch {
|
|
334
|
+
/* not laid out yet */
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
refit();
|
|
338
|
+
p.observer = new ResizeObserver(refit);
|
|
339
|
+
p.observer.observe(el);
|
|
340
|
+
const enc = new TextEncoder();
|
|
341
|
+
term.onData((d) => {
|
|
342
|
+
if (p.sock?.readyState === WebSocket.OPEN) p.sock.send(enc.encode(d));
|
|
343
|
+
});
|
|
344
|
+
const size = () => {
|
|
345
|
+
if (p.sock?.readyState === WebSocket.OPEN && term.cols > 1 && term.rows > 1)
|
|
346
|
+
p.sock.send(
|
|
347
|
+
JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }),
|
|
348
|
+
);
|
|
349
|
+
};
|
|
350
|
+
term.onResize(size);
|
|
351
|
+
const token = await csrf();
|
|
352
|
+
if (p.closed) return;
|
|
353
|
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
354
|
+
const sock = new WebSocket(
|
|
355
|
+
`${proto}//${location.host}/api/sessions/${encodeURIComponent(id)}/io`,
|
|
356
|
+
["den.io", `den.csrf.${token ?? ""}`],
|
|
357
|
+
);
|
|
358
|
+
sock.binaryType = "arraybuffer";
|
|
359
|
+
p.sock = sock;
|
|
360
|
+
sock.onopen = () => {
|
|
361
|
+
refit();
|
|
362
|
+
size();
|
|
363
|
+
term.focus();
|
|
364
|
+
};
|
|
365
|
+
sock.onmessage = (e) => {
|
|
366
|
+
if (typeof e.data === "string") {
|
|
367
|
+
let msg = null;
|
|
368
|
+
try {
|
|
369
|
+
msg = JSON.parse(e.data);
|
|
370
|
+
} catch {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (msg?.type === "exit") {
|
|
374
|
+
term.write(`\r\n${t("ag.exitedLine", { code: "" })}\r\n`);
|
|
375
|
+
setTimeout(() => void refresh(), 1000);
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
term.write(new Uint8Array(e.data));
|
|
380
|
+
};
|
|
381
|
+
sock.onclose = (e) => {
|
|
382
|
+
if (p.closed || p.sock !== sock) return;
|
|
383
|
+
p.sock = null;
|
|
384
|
+
term.write(`\r\n${t("ag.lost", { why: e.reason || String(e.code) })}\r\n`);
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function mountServerAgentPage(root) {
|
|
389
|
+
if (!root) return;
|
|
390
|
+
st.root = root;
|
|
391
|
+
for (const [sel, key] of [
|
|
392
|
+
["#sa-title", "n.serverAgent"],
|
|
393
|
+
["#sa-runtime > span", "sa.runsOn"],
|
|
394
|
+
["#sa-model > span", "sa.model"],
|
|
395
|
+
["#sa-open", "sa.openAgents"],
|
|
396
|
+
]) {
|
|
397
|
+
const el = root.querySelector(sel);
|
|
398
|
+
if (el) el.textContent = t(key);
|
|
399
|
+
}
|
|
400
|
+
if (window.denShell?.apiOff?.()) return;
|
|
401
|
+
root.addEventListener("click", (e) => {
|
|
402
|
+
const b = e.target.closest("button[data-act]");
|
|
403
|
+
if (b?.dataset.act === "start") {
|
|
404
|
+
b.disabled = true;
|
|
405
|
+
void start();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
root.querySelector("#sa-runtime select")?.addEventListener("change", (e) => {
|
|
409
|
+
void saveRuntime(e.target.value);
|
|
410
|
+
});
|
|
411
|
+
root.querySelector("#sa-model select")?.addEventListener("change", (e) => {
|
|
412
|
+
void saveModel(e.target.value);
|
|
413
|
+
});
|
|
414
|
+
await refresh();
|
|
415
|
+
}
|