@monster-orange/den 0.4.16 → 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 +101 -0
- package/README.md +7 -7
- package/docs/distributions.md +94 -1
- package/package.json +1 -1
- package/src/ui/cli/agent-ops.ts +50 -0
- package/src/ui/cli/agents-command.ts +5 -5
- package/src/ui/server/agent-routes.ts +13 -1
- package/src/ui/server/check-pool.ts +72 -0
- package/src/ui/server/index.ts +105 -10
- package/src/ui/server/public/agents-page.js +98 -16
- package/src/ui/server/public/agents.html +22 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/graph.css +40 -0
- package/src/ui/server/public/graph.js +47 -6
- package/src/ui/server/public/header.js +7 -0
- package/src/ui/server/public/i18n/es.js +46 -0
- package/src/ui/server/public/i18n/ja.js +46 -0
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +199 -27
- package/src/ui/server/public/log-source.js +26 -0
- package/src/ui/server/public/logs.html +7 -20
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/services.html +18 -3
- package/src/ui/server/public/shell.js +360 -32
- package/src/ui/server/public/svc-panel.js +672 -0
- package/src/ui/server/public/topbar.js +34 -1
- package/src/ui/server/public/view-scope.js +61 -0
- package/src/ui/server/public/workspace-picker.js +2 -0
- package/src/ui/server/route-classes.ts +2 -0
- package/src/ui/server/run-history.ts +181 -0
- package/src/ui/server/session-routes.ts +94 -17
- package/src/ui/server/ui-assets.ts +6 -0
package/src/ui/server/index.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { moduleScanner } from "../../shared/test-discovery/module-scanner.js";
|
|
|
31
31
|
import { resolveFieldEnv } from "../../shared/perf/optional-fields.js";
|
|
32
32
|
import { testRunner } from "../cli/test-runner.js";
|
|
33
33
|
import { discoverRepos } from "../../pillars/smoke/discover.js";
|
|
34
|
+
import { planSuites } from "../../pillars/suites/run.js";
|
|
35
|
+
import { listServicesWithManifests } from "../../shared/manifest/workspace-manifests.js";
|
|
34
36
|
import {
|
|
35
37
|
awsProfileEnv,
|
|
36
38
|
loadAwsProfiles,
|
|
@@ -856,6 +858,19 @@ import { heldActions } from "./held-actions.js";
|
|
|
856
858
|
import type { ApprovalQueue } from "./approvals.js";
|
|
857
859
|
import { processPathRules } from "../cli/onboard-reads.js";
|
|
858
860
|
import { handleAgentRoutes, startAgentUpkeep } from "./agent-routes.js";
|
|
861
|
+
import {
|
|
862
|
+
lastRunsByRepo,
|
|
863
|
+
recordRunEnd,
|
|
864
|
+
recordRunStart,
|
|
865
|
+
reposOfRun,
|
|
866
|
+
} from "./run-history.js";
|
|
867
|
+
import {
|
|
868
|
+
CHECK_CONCURRENCY,
|
|
869
|
+
CHECK_TIMEOUT_MS,
|
|
870
|
+
runCapped,
|
|
871
|
+
timedOutText,
|
|
872
|
+
withTimeout,
|
|
873
|
+
} from "./check-pool.js";
|
|
859
874
|
import {
|
|
860
875
|
handleSessionList,
|
|
861
876
|
handleSessionUpgrade,
|
|
@@ -1273,6 +1288,9 @@ function sshUserNow(): string {
|
|
|
1273
1288
|
return sshUser(set, osUser);
|
|
1274
1289
|
}
|
|
1275
1290
|
|
|
1291
|
+
/** Test runs this server is running now, by id (run-history.ts). */
|
|
1292
|
+
const LIVE_RUNS = new Set<string>();
|
|
1293
|
+
|
|
1276
1294
|
/** The ssh target for this request: the Host it used, minus the port. */
|
|
1277
1295
|
function sshTargetOf(req: http.IncomingMessage): string {
|
|
1278
1296
|
return sshTarget(req.headers.host, os.hostname());
|
|
@@ -1969,8 +1987,10 @@ async function handleRequest(
|
|
|
1969
1987
|
// AG-4a-term's session routes (`/api/sessions/<id>/io`): shell-grade, and
|
|
1970
1988
|
// an agent's token reaches its own session's only (request-guard). The io
|
|
1971
1989
|
// is a WebSocket (the server's upgrade handler): a plain GET of it is 426.
|
|
1972
|
-
if (pathname.startsWith("/api/sessions/"))
|
|
1973
|
-
|
|
1990
|
+
if (pathname.startsWith("/api/sessions/")) {
|
|
1991
|
+
await sendSessionHttp(req, res, pathname);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1974
1994
|
// ON-3: an agent's held writes; Allow from the person's browser only.
|
|
1975
1995
|
if (
|
|
1976
1996
|
guard.ok &&
|
|
@@ -2782,15 +2802,55 @@ async function handleRequest(
|
|
|
2782
2802
|
// Streaming emits in completion order, which varies per poll (shell probes
|
|
2783
2803
|
// finish at wildly different speeds), so without a stable key the client
|
|
2784
2804
|
// repaints the dots in a different order every 5s.
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2805
|
+
// 0.4.17: at most CHECK_CONCURRENCY at once, each with at most
|
|
2806
|
+
// CHECK_TIMEOUT_MS: a dead host's check says "timed out after 10 s" and
|
|
2807
|
+
// never holds up the run (check-pool.ts).
|
|
2808
|
+
const late = (e: (typeof sortedEntries)[number]): HealthResult => ({
|
|
2809
|
+
repo: e.repo,
|
|
2810
|
+
service: e.service,
|
|
2811
|
+
label: e.label,
|
|
2812
|
+
...(e.checkType !== "none"
|
|
2813
|
+
? healthCheckFields(e.checkType, e.target)
|
|
2814
|
+
: { url: null }),
|
|
2815
|
+
repoPath: e.repoPath,
|
|
2816
|
+
mode: e.mode,
|
|
2817
|
+
isEntrypoint: e.isEntrypoint,
|
|
2818
|
+
...e.meta,
|
|
2819
|
+
ms: CHECK_TIMEOUT_MS,
|
|
2820
|
+
ok: false,
|
|
2821
|
+
error: timedOutText(CHECK_TIMEOUT_MS),
|
|
2822
|
+
});
|
|
2823
|
+
const run = (e: (typeof sortedEntries)[number], idx: number) =>
|
|
2824
|
+
withTimeout(checkEntry(e), CHECK_TIMEOUT_MS, () => late(e)).then((r) => {
|
|
2825
|
+
const ranked = { ...r, openUrl: e.openUrl, links: e.links, idx };
|
|
2826
|
+
onResult?.(ranked);
|
|
2827
|
+
return ranked;
|
|
2828
|
+
});
|
|
2829
|
+
// Compose and container checks are answered in-process from one shared
|
|
2830
|
+
// docker listing (the call in flight): they start no process, so they
|
|
2831
|
+
// run outside the cap, all at once, and a cycle still lists once.
|
|
2832
|
+
const inProcess = (e: (typeof sortedEntries)[number]) =>
|
|
2833
|
+
(e.checkType === "container" && !!e.container) ||
|
|
2834
|
+
(e.checkType === "shell" && !!e.compose && e.target === e.compose.cmd);
|
|
2835
|
+
const indexed = sortedEntries.map((e, idx) => ({ e, idx }));
|
|
2836
|
+
const results = new Array<HealthResult>(sortedEntries.length);
|
|
2837
|
+
await Promise.all([
|
|
2838
|
+
...indexed
|
|
2839
|
+
.filter(({ e }) => inProcess(e))
|
|
2840
|
+
.map(({ e, idx }) =>
|
|
2841
|
+
run(e, idx).then((r) => {
|
|
2842
|
+
results[idx] = r;
|
|
2843
|
+
}),
|
|
2844
|
+
),
|
|
2845
|
+
runCapped(
|
|
2846
|
+
indexed.filter(({ e }) => !inProcess(e)),
|
|
2847
|
+
CHECK_CONCURRENCY,
|
|
2848
|
+
({ e, idx }) =>
|
|
2849
|
+
run(e, idx).then((r) => {
|
|
2850
|
+
results[idx] = r;
|
|
2851
|
+
}),
|
|
2792
2852
|
),
|
|
2793
|
-
);
|
|
2853
|
+
]);
|
|
2794
2854
|
return results;
|
|
2795
2855
|
}
|
|
2796
2856
|
|
|
@@ -3413,6 +3473,13 @@ async function handleRequest(
|
|
|
3413
3473
|
return;
|
|
3414
3474
|
}
|
|
3415
3475
|
|
|
3476
|
+
// ── API: each repo's last test run (0.4.17, run-history.ts) ─────────────────
|
|
3477
|
+
if (pathname === "/api/runs/last") {
|
|
3478
|
+
return sendJson(res, {
|
|
3479
|
+
repos: lastRunsByRepo(getStateDir(), LIVE_RUNS),
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3416
3483
|
// ── API: running daemons list ──────────────────────────────────────────────
|
|
3417
3484
|
if (pathname === "/api/service/daemons") {
|
|
3418
3485
|
await startupPrune;
|
|
@@ -3878,8 +3945,36 @@ async function handleRequest(
|
|
|
3878
3945
|
|
|
3879
3946
|
const startTime = Date.now();
|
|
3880
3947
|
let overallExit = 0;
|
|
3948
|
+
// 0.4.17: the server's run history (every browser's runs, kept), which
|
|
3949
|
+
// Workspaces' Last test run reads per repo.
|
|
3950
|
+
// Every run kind that runs a repo's own tests names it, the suites
|
|
3951
|
+
// pillar (`tests:`) and a repo's performance modules included.
|
|
3952
|
+
const tested = await reposOfRun(String(pillar), String(env), modules, {
|
|
3953
|
+
pillarSources: (p) =>
|
|
3954
|
+
collectTestSources(p as Pillar).map((x) => x.repoName),
|
|
3955
|
+
suiteRepos: (e, only) =>
|
|
3956
|
+
planSuites(listServicesWithManifests(), e as Environment, only).run.map(
|
|
3957
|
+
(x) => x.repo,
|
|
3958
|
+
),
|
|
3959
|
+
perfModules: async () =>
|
|
3960
|
+
(await moduleScanner.scanModules("performance")).map((m) => m.name),
|
|
3961
|
+
repoNames: () => discoverRepos().map((r) => r.repoName),
|
|
3962
|
+
});
|
|
3963
|
+
LIVE_RUNS.add(runId);
|
|
3964
|
+
recordRunStart(getStateDir(), {
|
|
3965
|
+
id: runId,
|
|
3966
|
+
pillar: String(pillar),
|
|
3967
|
+
env: String(env),
|
|
3968
|
+
repos: tested,
|
|
3969
|
+
});
|
|
3970
|
+
let recorded = false;
|
|
3881
3971
|
|
|
3882
3972
|
const finishLifecycle = (exitCode: number, note?: string): void => {
|
|
3973
|
+
if (!recorded) {
|
|
3974
|
+
recorded = true;
|
|
3975
|
+
LIVE_RUNS.delete(runId);
|
|
3976
|
+
recordRunEnd(getStateDir(), runId, exitCode);
|
|
3977
|
+
}
|
|
3883
3978
|
const dur = formatDuration(Date.now() - startTime);
|
|
3884
3979
|
const extra = note ? ` · ${note}` : "";
|
|
3885
3980
|
serverLog(
|
|
@@ -147,6 +147,9 @@ const st = {
|
|
|
147
147
|
signedOut: false,
|
|
148
148
|
open: [],
|
|
149
149
|
panes: new Map(),
|
|
150
|
+
// AG-18: agents (ws\0id) whose persona change started fresh, for the
|
|
151
|
+
// pane's line once it follows them.
|
|
152
|
+
fresh: new Set(),
|
|
150
153
|
timer: null,
|
|
151
154
|
xterm: null,
|
|
152
155
|
csrf: null,
|
|
@@ -200,6 +203,7 @@ function writeUrl() {
|
|
|
200
203
|
else u.searchParams.delete("open");
|
|
201
204
|
u.searchParams.set("layout", st.layout);
|
|
202
205
|
u.searchParams.delete("start");
|
|
206
|
+
u.searchParams.delete("folder");
|
|
203
207
|
const s = u.searchParams.toString().replace(/%2C/gi, ",");
|
|
204
208
|
history.replaceState(history.state, "", u.pathname + (s ? `?${s}` : ""));
|
|
205
209
|
paintSub();
|
|
@@ -326,6 +330,18 @@ function rotationWhy(rot) {
|
|
|
326
330
|
);
|
|
327
331
|
}
|
|
328
332
|
|
|
333
|
+
/** The Workspaces page, focused on workspace `ws` (0.4.17). */
|
|
334
|
+
function wsHref(ws) {
|
|
335
|
+
const nav = window.denShell?.nav?.() ?? [];
|
|
336
|
+
const base = nav.find((n) => n.id === "local-dev")?.href ?? "/local-dev";
|
|
337
|
+
return `${base}?ws=${encodeURIComponent(ws)}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Workspace `ws` as a link to it on Workspaces. */
|
|
341
|
+
function wsLinkHtml(ws) {
|
|
342
|
+
return `<a class="wslink" href="${esc(wsHref(ws))}" title="${esc(t("ag.goWs", { ws }))}">${esc(ws)}</a>`;
|
|
343
|
+
}
|
|
344
|
+
|
|
329
345
|
/** Its rotation, when one is due: "ready" or "due" (den#4). */
|
|
330
346
|
function rotationOf(r) {
|
|
331
347
|
if (!r.rotation?.due) return "";
|
|
@@ -480,7 +496,7 @@ function paintList() {
|
|
|
480
496
|
? groups
|
|
481
497
|
.map(
|
|
482
498
|
(g) =>
|
|
483
|
-
`<div class="wsg" data-ws="${esc(g.ws)}"><div class="wsh"><span>${
|
|
499
|
+
`<div class="wsg" data-ws="${esc(g.ws)}"><div class="wsh"><span>${wsLinkHtml(g.ws)}</span><span>${esc(g.rows.length)}</span></div>` +
|
|
484
500
|
g.rows.map(rowHtml).join("") +
|
|
485
501
|
`</div>`,
|
|
486
502
|
)
|
|
@@ -616,6 +632,11 @@ function followAgents() {
|
|
|
616
632
|
}
|
|
617
633
|
}
|
|
618
634
|
|
|
635
|
+
/** The pane's line for a resume that had nothing to continue (AG-14). */
|
|
636
|
+
function freshLine(p) {
|
|
637
|
+
p.term?.write(`\x1b[2m${t("ag.freshLine")}\x1b[0m\r\n`);
|
|
638
|
+
}
|
|
639
|
+
|
|
619
640
|
/** Move pane `p` to session row `row`, where it was. */
|
|
620
641
|
function retarget(p, row) {
|
|
621
642
|
const old = p.id;
|
|
@@ -639,6 +660,7 @@ function retarget(p, row) {
|
|
|
639
660
|
p.term?.write(
|
|
640
661
|
`\r\n\x1b[2m${t("ag.switched", { p: row.persona ?? row.agent })}\x1b[0m\r\n`,
|
|
641
662
|
);
|
|
663
|
+
if (st.fresh.delete(`${p.agentWs}\u0000${p.agent}`)) freshLine(p);
|
|
642
664
|
paintBar(p);
|
|
643
665
|
paintLayout();
|
|
644
666
|
writeUrl();
|
|
@@ -769,6 +791,7 @@ function paintLayout() {
|
|
|
769
791
|
if (n === 0) {
|
|
770
792
|
box.className = "agempty";
|
|
771
793
|
delete box.dataset.layout;
|
|
794
|
+
delete box.dataset.ws;
|
|
772
795
|
box.innerHTML = `<div>${esc(t("ag.empty"))}</div>`;
|
|
773
796
|
return;
|
|
774
797
|
}
|
|
@@ -776,6 +799,10 @@ function paintLayout() {
|
|
|
776
799
|
const shown = visibleIds();
|
|
777
800
|
box.className = `agp n${Math.max(1, shown.length)}${layout === "tabs" ? " tabs" : ""}`;
|
|
778
801
|
box.dataset.layout = layout;
|
|
802
|
+
// The front agent's workspace: ⌘K lists it first (0.4.17).
|
|
803
|
+
const front = rowOf(st.cur)?.workspace;
|
|
804
|
+
if (front) box.dataset.ws = front;
|
|
805
|
+
else delete box.dataset.ws;
|
|
779
806
|
const empty = box.querySelector(":scope > div:not(.pane):not(.agtabs)");
|
|
780
807
|
if (empty) empty.remove();
|
|
781
808
|
let strip = box.querySelector(":scope > .agtabs");
|
|
@@ -919,10 +946,14 @@ function paintBar(p) {
|
|
|
919
946
|
const r = rowOf(p.id);
|
|
920
947
|
const bar = p.el.querySelector(".bar2");
|
|
921
948
|
const name = r ? paneName(r) : p.label || p.id;
|
|
922
|
-
// AG-4b: the session's name last, when it says more.
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
949
|
+
// AG-4b: the session's name last, when it says more. The workspace is a
|
|
950
|
+
// link to it on Workspaces (0.4.17).
|
|
951
|
+
const ws = r ? r.workspace : p.workspace || "";
|
|
952
|
+
const rest = r
|
|
953
|
+
? [r.runtime, sessionExtra(r)].filter(Boolean).join(" · ")
|
|
954
|
+
: "";
|
|
955
|
+
const where =
|
|
956
|
+
(ws ? wsLinkHtml(ws) : "") + (ws && rest ? " · " : "") + esc(rest);
|
|
926
957
|
const tone = r ? stateTone(r) : p.exited ? "off" : "pending";
|
|
927
958
|
const stop =
|
|
928
959
|
r?.kind === "agent" && (r.state === "running" || r.state === "stopping")
|
|
@@ -943,7 +974,7 @@ function paintBar(p) {
|
|
|
943
974
|
: "";
|
|
944
975
|
bar.innerHTML =
|
|
945
976
|
`<span class="den-dot ${tone}" aria-hidden="true"></span>` +
|
|
946
|
-
`<b>${esc(name)}</b><span class="where mono">${
|
|
977
|
+
`<b>${esc(name)}</b><span class="where mono">${where}</span>` +
|
|
947
978
|
(r?.tokenRevoked
|
|
948
979
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
949
980
|
: "") +
|
|
@@ -1124,21 +1155,41 @@ async function connect(p) {
|
|
|
1124
1155
|
}
|
|
1125
1156
|
p.term?.write(new Uint8Array(e.data));
|
|
1126
1157
|
};
|
|
1127
|
-
sock.onclose = (e) => {
|
|
1158
|
+
sock.onclose = async (e) => {
|
|
1128
1159
|
if (p.sock !== sock) return;
|
|
1129
1160
|
p.sock = null;
|
|
1130
1161
|
if (p.closed) return;
|
|
1131
|
-
// Refused before it opened:
|
|
1132
|
-
//
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1162
|
+
// Refused before it opened: a browser sees only the close, so ask the
|
|
1163
|
+
// route why with a plain GET (0.4.17). "Sign in" only when it's 401.
|
|
1164
|
+
let why = e.reason || String(e.code);
|
|
1165
|
+
if (!opened) {
|
|
1166
|
+
void refresh();
|
|
1167
|
+
why = await refusalReason(p.id);
|
|
1168
|
+
if (p.closed || p.sock) return;
|
|
1169
|
+
}
|
|
1136
1170
|
p.term?.write(`\r\n${t("ag.lost", { why })}\r\n`);
|
|
1137
1171
|
p.lost = true;
|
|
1138
1172
|
paintBar(p);
|
|
1139
1173
|
};
|
|
1140
1174
|
}
|
|
1141
1175
|
|
|
1176
|
+
/** Why session `id`'s io upgrade was refused, in words for its pane. */
|
|
1177
|
+
async function refusalReason(id) {
|
|
1178
|
+
try {
|
|
1179
|
+
const r = await fetch(`/api/sessions/${encodeURIComponent(id)}/io`, {
|
|
1180
|
+
headers: { accept: "application/json" },
|
|
1181
|
+
credentials: "same-origin",
|
|
1182
|
+
});
|
|
1183
|
+
if (r.status === 401) return t("ag.signin", { cli: CLI });
|
|
1184
|
+
const b = await r.json().catch(() => null);
|
|
1185
|
+
if (r.status === 426) return t("ag.refusedUnknown");
|
|
1186
|
+
if (typeof b?.error === "string" && b.error) return b.error;
|
|
1187
|
+
return t("ag.refusedStatus", { status: r.status });
|
|
1188
|
+
} catch {
|
|
1189
|
+
return t("ag.refusedUnknown");
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1142
1193
|
// ── links and keys in a pane (AG-4b) ───────────────────────────────────
|
|
1143
1194
|
|
|
1144
1195
|
/** What Shift+Enter sends to a pane's session (a plain session: Enter). */
|
|
@@ -2051,8 +2102,16 @@ function dialogMode(bring, change = null) {
|
|
|
2051
2102
|
);
|
|
2052
2103
|
$d("#ag-go").textContent = goLabel();
|
|
2053
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
|
+
}
|
|
2054
2110
|
}
|
|
2055
2111
|
|
|
2112
|
+
/** 0.4.18: Start agent here (the service panel): ?start=1&ws=…&folder=…. */
|
|
2113
|
+
let startPrefill = null;
|
|
2114
|
+
|
|
2056
2115
|
/** The submit button's words for the dialog's mode. */
|
|
2057
2116
|
function goLabel() {
|
|
2058
2117
|
return t(sd.change ? "ag.pc.go" : sd.bring ? "ag.bring.go" : "ag.go");
|
|
@@ -2074,12 +2133,18 @@ async function openStart(bring = false, change = null) {
|
|
|
2074
2133
|
const dlg = $d("#ag-dlg");
|
|
2075
2134
|
if (!dlg || dlg.open) return;
|
|
2076
2135
|
dialogMode(bring, change);
|
|
2136
|
+
const pre = change || bring ? null : startPrefill;
|
|
2137
|
+
startPrefill = null;
|
|
2077
2138
|
const shown = change ? [change.workspace] : viewWorkspaces();
|
|
2139
|
+
if (pre?.ws && !shown.includes(pre.ws)) shown.push(pre.ws);
|
|
2078
2140
|
const wsSel = $d("#ag-ws");
|
|
2079
2141
|
wsSel.innerHTML = shown
|
|
2080
2142
|
.map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
|
|
2081
2143
|
.join("");
|
|
2082
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 ?? "";
|
|
2083
2148
|
sd.f = { q: "", cat: "", src: "" };
|
|
2084
2149
|
sd.det = false;
|
|
2085
2150
|
sd.what = false;
|
|
@@ -2120,6 +2185,8 @@ function startBody() {
|
|
|
2120
2185
|
if (task) body.instruction = task;
|
|
2121
2186
|
const name = $d("#ag-name").value.trim();
|
|
2122
2187
|
if (name) body.name = name;
|
|
2188
|
+
const folder = $d("#ag-folder")?.value.trim();
|
|
2189
|
+
if (folder) body.folder = folder;
|
|
2123
2190
|
return body;
|
|
2124
2191
|
}
|
|
2125
2192
|
|
|
@@ -2306,11 +2373,20 @@ async function submitPersonaChange(when) {
|
|
|
2306
2373
|
body,
|
|
2307
2374
|
);
|
|
2308
2375
|
window.denShell?.toast?.(
|
|
2309
|
-
t(
|
|
2310
|
-
|
|
2311
|
-
p: personaName(p),
|
|
2312
|
-
|
|
2376
|
+
t(
|
|
2377
|
+
r?.pending ? "ag.pc.pending" : r?.fresh ? "ag.pc.fresh" : "ag.pc.done",
|
|
2378
|
+
{ id: ch.name, p: personaName(p) },
|
|
2379
|
+
),
|
|
2313
2380
|
);
|
|
2381
|
+
// Never prompted: nothing was resumed. Its pane says so, now if it
|
|
2382
|
+
// has already moved to the new session, else when it does.
|
|
2383
|
+
if (r?.fresh) {
|
|
2384
|
+
const pane = [...st.panes.values()].find(
|
|
2385
|
+
(q) => q.agent === ch.id && q.agentWs === ch.workspace,
|
|
2386
|
+
);
|
|
2387
|
+
if (pane && pane.id === r.agent?.sessionId) freshLine(pane);
|
|
2388
|
+
else st.fresh.add(`${ch.workspace}\u0000${ch.id}`);
|
|
2389
|
+
}
|
|
2314
2390
|
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
2315
2391
|
dlg.close?.();
|
|
2316
2392
|
await refresh();
|
|
@@ -2569,6 +2645,8 @@ function translateStatic(root) {
|
|
|
2569
2645
|
set("#ag-what-p", "pe.what");
|
|
2570
2646
|
set("#ag-task-t", "ag.new.task");
|
|
2571
2647
|
set("#ag-name-t", "ag.new.name");
|
|
2648
|
+
set("#ag-folder-t", "ag.new.folder");
|
|
2649
|
+
set("#ag-folder-h", "ag.new.folderHelp");
|
|
2572
2650
|
set("#ag-adv-s", "ag.new.adv");
|
|
2573
2651
|
set("#ag-note", "ag.new.note");
|
|
2574
2652
|
const task = root.querySelector("#ag-task");
|
|
@@ -2600,6 +2678,10 @@ export async function mountAgentsPage(root) {
|
|
|
2600
2678
|
const wanted = openFromUrl(location.search);
|
|
2601
2679
|
const linked = layoutFromUrl(location.search);
|
|
2602
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
|
+
}
|
|
2603
2685
|
|
|
2604
2686
|
root.querySelector("#agl").addEventListener("click", (e) => {
|
|
2605
2687
|
const act = e.target.closest("button[data-act]");
|
|
@@ -412,6 +412,15 @@
|
|
|
412
412
|
flex: none;
|
|
413
413
|
white-space: nowrap;
|
|
414
414
|
}
|
|
415
|
+
.wslink {
|
|
416
|
+
color: inherit;
|
|
417
|
+
text-decoration: none;
|
|
418
|
+
}
|
|
419
|
+
.wslink:hover,
|
|
420
|
+
.wslink:focus-visible {
|
|
421
|
+
color: var(--text);
|
|
422
|
+
text-decoration: underline;
|
|
423
|
+
}
|
|
415
424
|
.pane .bar2 .where {
|
|
416
425
|
flex: 0 1 auto;
|
|
417
426
|
min-width: 0;
|
|
@@ -730,6 +739,19 @@
|
|
|
730
739
|
><span id="ag-name-t">Name (optional)</span>
|
|
731
740
|
<input id="ag-name" maxlength="40" autocomplete="off" />
|
|
732
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>
|
|
733
755
|
<p class="helpl" id="ag-as" role="status"></p>
|
|
734
756
|
<details id="ag-adv">
|
|
735
757
|
<summary id="ag-adv-s">Advanced: model and permissions</summary>
|