@monster-orange/den 0.4.11 → 0.4.13
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 +169 -0
- package/README.md +7 -7
- package/docs/distributions.md +212 -12
- package/package.json +2 -1
- package/src/shared/agents/catalog.ts +77 -0
- package/src/shared/agents/default-pack/be-dev.md +1 -0
- package/src/shared/agents/default-pack/devops.md +1 -0
- package/src/shared/agents/default-pack/fe-dev.md +1 -0
- package/src/shared/agents/default-pack/orchestrator.md +3 -1
- package/src/shared/agents/default-pack/planner.md +1 -0
- package/src/shared/agents/default-pack/reviewer.md +1 -0
- package/src/shared/agents/default-pack/tester.md +16 -0
- package/src/shared/agents/default-pack/ux-designer.md +1 -0
- package/src/shared/agents/den-claude.ts +151 -17
- package/src/shared/agents/permissions.ts +108 -0
- package/src/shared/agents/persona-store.ts +286 -0
- package/src/shared/agents/personas.ts +116 -12
- package/src/shared/agents/records.ts +57 -0
- package/src/shared/agents/roles.ts +288 -0
- package/src/shared/agents/runtime.ts +49 -0
- package/src/shared/agents/session-name.ts +110 -0
- package/src/shared/agents/transcript-follow.ts +180 -0
- package/src/shared/den-config.ts +27 -1
- package/src/ui/cli/agent-ops.ts +578 -11
- package/src/ui/cli/agents-command.ts +12 -2
- package/src/ui/cli/env-probe.ts +4 -1
- package/src/ui/mcp/tools.ts +1 -1
- package/src/ui/server/agent-paths.ts +52 -0
- package/src/ui/server/agent-routes.ts +329 -19
- package/src/ui/server/agents-layout-pref.ts +43 -0
- package/src/ui/server/editor-open-pref.ts +41 -0
- package/src/ui/server/index.ts +22 -0
- package/src/ui/server/public/agent-start.js +3 -2
- package/src/ui/server/public/agents-page.js +1037 -67
- package/src/ui/server/public/agents-section.js +1 -1
- package/src/ui/server/public/agents.html +236 -20
- package/src/ui/server/public/header.js +2 -0
- package/src/ui/server/public/i18n/es.js +139 -0
- package/src/ui/server/public/i18n/ja.js +139 -0
- package/src/ui/server/public/persona-ui.css +539 -0
- package/src/ui/server/public/persona-ui.js +796 -0
- package/src/ui/server/public/settings-page.js +4 -9
- package/src/ui/server/public/settings-registry.js +66 -0
- package/src/ui/server/public/settings.html +1 -0
- package/src/ui/server/public/shell.js +137 -0
- package/src/ui/server/public/term-input.js +129 -0
- package/src/ui/server/public/topbar.js +22 -4
- package/src/ui/server/route-classes.ts +29 -0
- package/src/ui/server/session-routes.ts +34 -1
- package/src/ui/server/ui-assets.ts +8 -0
|
@@ -27,8 +27,17 @@
|
|
|
27
27
|
* Nothing here keeps a session's state: the list is GET /api/sessions,
|
|
28
28
|
* asked again every few seconds while the page is shown.
|
|
29
29
|
*
|
|
30
|
-
* Start agent (AG-
|
|
31
|
-
*
|
|
30
|
+
* Start agent (AG-10.1): a searchable persona list (category and source
|
|
31
|
+
* chips, grouped by category, Plain session pinned last), What's a
|
|
32
|
+
* persona?, Details, + New persona, Duplicate or Edit (persona-ui.js),
|
|
33
|
+
* Runtime and Workspace, a first instruction (typed into the session once
|
|
34
|
+
* it is ready, by the server: never an argument), and Advanced: model and
|
|
35
|
+
* version (AG-12), permission mode and extra allowed commands with the
|
|
36
|
+
* preview of what the runtime is given (AG-13). What differs from the
|
|
37
|
+
* persona tags the session's row (AG-10.9). The runtime line says what
|
|
38
|
+
* will run, or why it can't, before Start (agent-start.js readiness); a
|
|
39
|
+
* persona whose runtime isn't installed starts on the first one that is,
|
|
40
|
+
* and the dialog says so (AG-12.2). Starting, stopping and
|
|
32
41
|
* resuming an agent never ask for the admin password: it is for sudo, and
|
|
33
42
|
* no agent action runs sudo (LK-0).
|
|
34
43
|
*
|
|
@@ -41,6 +50,30 @@
|
|
|
41
50
|
* agent in view. After a restart, one line says how many were interrupted,
|
|
42
51
|
* with Resume all and Review.
|
|
43
52
|
*
|
|
53
|
+
* Layouts (AG-4b): Focus (the default: one pane; a click replaces it,
|
|
54
|
+
* Shift- or ⌘-click adds one alongside), Tabs (one pane under a strip of
|
|
55
|
+
* named tabs) or Tile (up to four in a grid: 1 full, 2 side by side, 3-4
|
|
56
|
+
* as 2x2). The page header's switch, ⌃⇧1 / ⌃⇧2 / ⌃⇧3 (Ctrl+Shift+digit
|
|
57
|
+
* on every OS: ⌘1-9 and Ctrl+1-9 are the browser's tabs) and ⌘K "Layout:
|
|
58
|
+
* …" pick one; it is the person's (POST /api/settings agents_layout) and the URL's (&layout=; a
|
|
59
|
+
* link's wins for that page and saves nothing). At phone width Focus and
|
|
60
|
+
* Tile are Tabs. Every pane open stays attached; a pane out of sight never
|
|
61
|
+
* resizes its PTY. Double-click a tab to rename the agent: the name is its
|
|
62
|
+
* record's (POST …/agents/<id>/name), shown in the list, the pane, ⌘K.
|
|
63
|
+
*
|
|
64
|
+
* The terminal (AG-4b): Shift+Enter sends the runtime's soft newline (its
|
|
65
|
+
* catalog softNewline, from /api/sessions: ESC CR, Codex's Ctrl+J; a
|
|
66
|
+
* plain session's is Enter) and nothing else. The layout keys never reach
|
|
67
|
+
* the agent. Links: plain URLs (addon-web-links) and OSC 8 links open on
|
|
68
|
+
* ⌘-click (Ctrl-click elsewhere) in a new tab with noopener, http and
|
|
69
|
+
* https only; a plain click selects as ever. A file path is a link when
|
|
70
|
+
* the server says it exists inside the agent's workspace (GET …/paths,
|
|
71
|
+
* stat only): ⌘-click opens it in Settings' editor_open (vscode, cursor,
|
|
72
|
+
* zed, by URL scheme, at the line), or, with none (the default), copies
|
|
73
|
+
* its absolute path. The session's name (its runtime's /rename, or Den's
|
|
74
|
+
* rename, whichever is later) follows the runtime in the pane header and
|
|
75
|
+
* the persona in its tab, when it says more than the agent's own name.
|
|
76
|
+
*
|
|
44
77
|
* Session rotation (den#4): an agent whose session rotated (after
|
|
45
78
|
* agents.rotate.after_compactions compactions, or its pull request merged)
|
|
46
79
|
* and whose handoff.md passes the check reads "Ready to relaunch (3
|
|
@@ -50,7 +83,35 @@
|
|
|
50
83
|
* and Start fresh. A running one says "Handoff due" or "Ready to relaunch"
|
|
51
84
|
* as a tag.
|
|
52
85
|
*/
|
|
53
|
-
import {
|
|
86
|
+
import { readiness, runtimeName, runtimeOptions } from "/agent-start.js";
|
|
87
|
+
import {
|
|
88
|
+
commandProblem,
|
|
89
|
+
filterBarHtml,
|
|
90
|
+
modelOptionsHtml,
|
|
91
|
+
normalizeData,
|
|
92
|
+
openPersonaEditor,
|
|
93
|
+
overrideTagsHtml,
|
|
94
|
+
permBlockHtml,
|
|
95
|
+
personaDetail,
|
|
96
|
+
personaDetailHtml,
|
|
97
|
+
personaListHtml,
|
|
98
|
+
personaName,
|
|
99
|
+
personaRowHtml,
|
|
100
|
+
personaRuntime,
|
|
101
|
+
runtimeInstalled,
|
|
102
|
+
splitCommands,
|
|
103
|
+
syncPermBlock,
|
|
104
|
+
versionLabel,
|
|
105
|
+
versionOptionsHtml,
|
|
106
|
+
} from "/persona-ui.js";
|
|
107
|
+
import {
|
|
108
|
+
editorUrl,
|
|
109
|
+
findPaths,
|
|
110
|
+
isWebUrl,
|
|
111
|
+
layoutKeyIndex,
|
|
112
|
+
linkModifier,
|
|
113
|
+
softNewlineKey,
|
|
114
|
+
} from "/term-input.js";
|
|
54
115
|
|
|
55
116
|
const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
|
|
56
117
|
const esc = (s) =>
|
|
@@ -69,6 +130,13 @@ export const UNDO_MS = 10_000;
|
|
|
69
130
|
|
|
70
131
|
/** The most panes at once (the prototype's 2x2). */
|
|
71
132
|
export const MAX_PANES = 4;
|
|
133
|
+
|
|
134
|
+
/** The layouts (AG-4b), in the switch's order (⌃⇧1, ⌃⇧2, ⌃⇧3). */
|
|
135
|
+
export const LAYOUTS = ["focus", "tabs", "tile"];
|
|
136
|
+
/** At or below this width Focus and Tile are Tabs (the shell's phone width). */
|
|
137
|
+
const PHONE = "(max-width: 720px)";
|
|
138
|
+
/** The longest name a tab takes (records.ts MAX_DISPLAY_NAME). */
|
|
139
|
+
const MAX_NAME = 40;
|
|
72
140
|
const REFRESH_MS = 5_000;
|
|
73
141
|
|
|
74
142
|
const st = {
|
|
@@ -83,8 +151,37 @@ const st = {
|
|
|
83
151
|
xterm: null,
|
|
84
152
|
csrf: null,
|
|
85
153
|
docWired: false,
|
|
154
|
+
// AG-4b: the person's layout, the pane in front, and in Focus the panes
|
|
155
|
+
// Shift-clicked in alongside it.
|
|
156
|
+
layout: "focus",
|
|
157
|
+
cur: null,
|
|
158
|
+
side: new Set(),
|
|
159
|
+
// AG-4b: where a path link opens (Settings editor_open).
|
|
160
|
+
editorOpen: "none",
|
|
86
161
|
};
|
|
87
162
|
|
|
163
|
+
/** ?layout=, when it names one; else null. */
|
|
164
|
+
export function layoutFromUrl(search) {
|
|
165
|
+
const l = new URLSearchParams(search).get("layout");
|
|
166
|
+
return LAYOUTS.includes(l) ? l : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const phone = () => !!window.matchMedia?.(PHONE).matches;
|
|
170
|
+
|
|
171
|
+
/** The layout drawn: the person's, except at phone width (Tabs). */
|
|
172
|
+
function shownLayout() {
|
|
173
|
+
return phone() ? "tabs" : st.layout;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The panes in sight, in the URL's order. */
|
|
177
|
+
function visibleIds() {
|
|
178
|
+
const l = shownLayout();
|
|
179
|
+
if (l === "tile") return [...st.open];
|
|
180
|
+
if (!st.open.includes(st.cur)) st.cur = st.open.at(-1) ?? null;
|
|
181
|
+
if (l === "tabs") return st.cur ? [st.cur] : [];
|
|
182
|
+
return st.open.filter((id) => id === st.cur || st.side.has(id));
|
|
183
|
+
}
|
|
184
|
+
|
|
88
185
|
// ── the URL ─────────────────────────────────────────────────────────────
|
|
89
186
|
|
|
90
187
|
/** The ids ?open= names (at most MAX_PANES, in order, no repeats). */
|
|
@@ -101,6 +198,7 @@ function writeUrl() {
|
|
|
101
198
|
const u = new URL(location.href);
|
|
102
199
|
if (st.open.length) u.searchParams.set("open", st.open.join(","));
|
|
103
200
|
else u.searchParams.delete("open");
|
|
201
|
+
u.searchParams.set("layout", st.layout);
|
|
104
202
|
u.searchParams.delete("start");
|
|
105
203
|
const s = u.searchParams.toString().replace(/%2C/gi, ",");
|
|
106
204
|
history.replaceState(history.state, "", u.pathname + (s ? `?${s}` : ""));
|
|
@@ -147,7 +245,34 @@ function stateText(state) {
|
|
|
147
245
|
}
|
|
148
246
|
|
|
149
247
|
function displayName(r) {
|
|
150
|
-
|
|
248
|
+
if (r.kind !== "agent") return r.label;
|
|
249
|
+
return r.displayName || (r.persona ?? r.label);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A row's name: the name a person gave it (AG-4b), then its persona and
|
|
254
|
+
* id; else its persona, then its id (when they differ).
|
|
255
|
+
*/
|
|
256
|
+
function nameHtml(r) {
|
|
257
|
+
const extra = sessionExtra(r);
|
|
258
|
+
if (r.displayName)
|
|
259
|
+
return `${esc(r.displayName)} <span class="id">${esc([r.persona, r.label, extra].filter(Boolean).join(" · "))}</span>`;
|
|
260
|
+
const id = [r.persona && r.persona !== r.label ? r.label : "", extra]
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
.join(" · ");
|
|
263
|
+
return `${esc(displayName(r))}${id ? ` <span class="id">${esc(id)}</span>` : ""}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The session's name (AG-4b: the runtime's /rename or Den's, the later),
|
|
268
|
+
* where it says more than the agent's own name: not empty, not the
|
|
269
|
+
* persona, not the name already shown. Else "".
|
|
270
|
+
*/
|
|
271
|
+
function sessionExtra(r) {
|
|
272
|
+
const n = r?.kind === "agent" ? (r.sessionTitle ?? "").trim() : "";
|
|
273
|
+
if (!n || n === r.persona || n === paneName(r) || n === displayName(r))
|
|
274
|
+
return "";
|
|
275
|
+
return n;
|
|
151
276
|
}
|
|
152
277
|
|
|
153
278
|
// A pane's title and its Close: the agent's display name (AG-10.8), else
|
|
@@ -211,7 +336,7 @@ function rotationOf(r) {
|
|
|
211
336
|
function stoppedHtml(r) {
|
|
212
337
|
const open = st.open.includes(r.id);
|
|
213
338
|
const name = displayName(r);
|
|
214
|
-
const nm =
|
|
339
|
+
const nm = nameHtml(r);
|
|
215
340
|
const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
|
|
216
341
|
const who = esc(r.agent);
|
|
217
342
|
const rot = rotationOf(r);
|
|
@@ -264,7 +389,7 @@ function rowHtml(r) {
|
|
|
264
389
|
const open = st.open.includes(r.id);
|
|
265
390
|
const nm =
|
|
266
391
|
r.kind === "agent"
|
|
267
|
-
?
|
|
392
|
+
? nameHtml(r)
|
|
268
393
|
: `${esc(r.label)} <span class="sbdg">${esc(t("ag.session"))}</span>`;
|
|
269
394
|
const right = r.waiting
|
|
270
395
|
? `<span class="wbdg">${esc(t("ag.waiting"))}</span>`
|
|
@@ -281,15 +406,16 @@ function rowHtml(r) {
|
|
|
281
406
|
const revokedTag = r.tokenRevoked
|
|
282
407
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
283
408
|
: "";
|
|
409
|
+
const ovTags = r.kind === "agent" ? overrideTagsHtml(r) : "";
|
|
284
410
|
const tags =
|
|
285
|
-
rotTag || revokedTag
|
|
286
|
-
? `<span class="agtags">${revokedTag}${rotTag}</span>`
|
|
411
|
+
rotTag || revokedTag || ovTags
|
|
412
|
+
? `<span class="agtags">${revokedTag}${rotTag}${ovTags}</span>`
|
|
287
413
|
: "";
|
|
288
414
|
return (
|
|
289
415
|
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="${esc(r.kind)}" data-state="${esc(r.state)}"` +
|
|
290
416
|
` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
|
|
291
417
|
` aria-label="${esc(t("ag.openName", { id: `${r.workspace} / ${displayName(r)}` }))}">` +
|
|
292
|
-
`<span class="den-dot ${stateTone(r)}" aria-hidden="true"></span>` +
|
|
418
|
+
`<span class="den-dot ${r.waiting ? "warn" : stateTone(r)}" aria-hidden="true"></span>` +
|
|
293
419
|
`<span class="nm">${nm}</span>${right}` +
|
|
294
420
|
(meta ? `<span class="meta">${esc(meta)}</span>` : "") +
|
|
295
421
|
tags +
|
|
@@ -360,7 +486,10 @@ function paintList() {
|
|
|
360
486
|
)
|
|
361
487
|
.join("")
|
|
362
488
|
: `<div class="none">${esc(t("ag.none.scope", { scope: st.scopeLabel }))}</div>`;
|
|
363
|
-
el.innerHTML =
|
|
489
|
+
el.innerHTML =
|
|
490
|
+
notes.join("") +
|
|
491
|
+
body +
|
|
492
|
+
`<div class="aghint" id="agl-hint">${esc(t(`ag.hint.${shownLayout()}`))} ${esc(t("ag.hint.keys"))}</div>`;
|
|
364
493
|
const clear = st.root.querySelector("#ag-clear");
|
|
365
494
|
if (clear) clear.hidden = !visibleRows().some(isStopped);
|
|
366
495
|
// No such workspace: nothing to start an agent in.
|
|
@@ -388,6 +517,21 @@ function paintSub() {
|
|
|
388
517
|
if (url) url.textContent = location.pathname + location.search;
|
|
389
518
|
}
|
|
390
519
|
|
|
520
|
+
/**
|
|
521
|
+
* The person's layout (GET /api/settings agents_layout), else Focus; and
|
|
522
|
+
* where a path link opens (editor_open), else none.
|
|
523
|
+
*/
|
|
524
|
+
async function savedLayout() {
|
|
525
|
+
try {
|
|
526
|
+
const r = await fetch("/api/settings", { headers: { accept: "json" } });
|
|
527
|
+
const b = r.ok ? await r.json() : null;
|
|
528
|
+
if (typeof b?.editor_open === "string") st.editorOpen = b.editor_open;
|
|
529
|
+
return LAYOUTS.includes(b?.agents_layout) ? b.agents_layout : "focus";
|
|
530
|
+
} catch {
|
|
531
|
+
return "focus";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
391
535
|
async function loadScope() {
|
|
392
536
|
try {
|
|
393
537
|
const r = await fetch("/api/scope");
|
|
@@ -425,6 +569,8 @@ async function refresh() {
|
|
|
425
569
|
paintList();
|
|
426
570
|
paintSub();
|
|
427
571
|
for (const p of st.panes.values()) paintBar(p);
|
|
572
|
+
const strip = st.root.querySelector("#agr > .agtabs");
|
|
573
|
+
if (strip) paintTabs(strip);
|
|
428
574
|
if (!st.root.isConnected) return;
|
|
429
575
|
st.timer = setTimeout(() => {
|
|
430
576
|
if (!document.hidden) void refresh();
|
|
@@ -440,7 +586,12 @@ async function loadXterm() {
|
|
|
440
586
|
st.xterm ??= Promise.all([
|
|
441
587
|
import("/vendor/xterm.mjs"),
|
|
442
588
|
import("/vendor/addon-fit.mjs"),
|
|
443
|
-
|
|
589
|
+
import("/vendor/addon-web-links.mjs"),
|
|
590
|
+
]).then(([x, f, w]) => ({
|
|
591
|
+
Terminal: x.Terminal,
|
|
592
|
+
FitAddon: f.FitAddon,
|
|
593
|
+
WebLinksAddon: w.WebLinksAddon,
|
|
594
|
+
}));
|
|
444
595
|
return st.xterm;
|
|
445
596
|
}
|
|
446
597
|
|
|
@@ -479,30 +630,233 @@ function termTheme() {
|
|
|
479
630
|
return theme;
|
|
480
631
|
}
|
|
481
632
|
|
|
633
|
+
/** A tab's words: the agent's name, then its persona and workspace. */
|
|
634
|
+
function tabWords(id) {
|
|
635
|
+
const r = rowOf(id);
|
|
636
|
+
const p = st.panes.get(id);
|
|
637
|
+
if (!r) return { name: p?.label || id, meta: p?.workspace || "" };
|
|
638
|
+
if (r.kind !== "agent")
|
|
639
|
+
return {
|
|
640
|
+
name: r.label,
|
|
641
|
+
meta: [t("ag.session"), r.workspace].join(" · "),
|
|
642
|
+
};
|
|
643
|
+
return {
|
|
644
|
+
name: paneName(r),
|
|
645
|
+
meta: [r.persona, r.workspace, sessionExtra(r)].filter(Boolean).join(" · "),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** The tab strip (AG-4b Tabs): one tab per open pane, its × beside it. */
|
|
650
|
+
function paintTabs(strip) {
|
|
651
|
+
// A tab being renamed keeps its input.
|
|
652
|
+
if (strip.querySelector("input")) return;
|
|
653
|
+
const have = [...strip.querySelectorAll(":scope > .agtab-w")].map(
|
|
654
|
+
(w) => w.dataset.id,
|
|
655
|
+
);
|
|
656
|
+
// The same tabs: changed in place, so a double-click's target stays.
|
|
657
|
+
if (have.join("\n") === st.open.join("\n")) {
|
|
658
|
+
for (const w of strip.querySelectorAll(":scope > .agtab-w")) {
|
|
659
|
+
const id = w.dataset.id;
|
|
660
|
+
const r = rowOf(id);
|
|
661
|
+
const words = tabWords(id);
|
|
662
|
+
const on = id === st.cur;
|
|
663
|
+
const tab = w.querySelector('[role="tab"]');
|
|
664
|
+
tab.setAttribute("aria-selected", String(on));
|
|
665
|
+
tab.tabIndex = on ? 0 : -1;
|
|
666
|
+
tab.querySelector(".den-dot").className =
|
|
667
|
+
`den-dot ${r?.waiting ? "warn" : r ? stateTone(r) : "pending"}`;
|
|
668
|
+
tab.querySelector(".tn").textContent = words.name;
|
|
669
|
+
tab.querySelector(".tm").textContent = words.meta;
|
|
670
|
+
const sr = tab.querySelector(".den-sr");
|
|
671
|
+
if (sr) sr.hidden = !r?.waiting;
|
|
672
|
+
w.querySelector(".agtab-x").setAttribute(
|
|
673
|
+
"aria-label",
|
|
674
|
+
t("ag.detachName", { id: words.name }),
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
strip.innerHTML = st.open
|
|
680
|
+
.map((id) => {
|
|
681
|
+
const r = rowOf(id);
|
|
682
|
+
const w = tabWords(id);
|
|
683
|
+
const on = id === st.cur;
|
|
684
|
+
const tone = r?.waiting ? "warn" : r ? stateTone(r) : "pending";
|
|
685
|
+
const rename =
|
|
686
|
+
r?.kind === "agent" ? ` title="${esc(t("ag.renameTip"))}"` : "";
|
|
687
|
+
return (
|
|
688
|
+
`<span class="agtab-w" data-id="${esc(id)}">` +
|
|
689
|
+
`<button type="button" role="tab" class="agtab" data-id="${esc(id)}" aria-selected="${on}" tabindex="${on ? 0 : -1}"${rename}>` +
|
|
690
|
+
`<span class="den-dot ${tone}" aria-hidden="true"></span>` +
|
|
691
|
+
`<span class="den-sr"${r?.waiting ? "" : " hidden"}>${esc(t("ag.waiting"))}: </span>` +
|
|
692
|
+
`<span class="tn">${esc(w.name)}</span><span class="tm mono">${esc(w.meta)}</span></button>` +
|
|
693
|
+
`<button type="button" class="agtab-x" data-act="close" data-id="${esc(id)}" aria-label="${esc(t("ag.detachName", { id: w.name }))}" title="${esc(t("ag.detach"))}">×</button>` +
|
|
694
|
+
`</span>`
|
|
695
|
+
);
|
|
696
|
+
})
|
|
697
|
+
.join("");
|
|
698
|
+
}
|
|
699
|
+
|
|
482
700
|
function paintLayout() {
|
|
483
701
|
const box = st.root.querySelector("#agr");
|
|
484
702
|
const n = st.open.length;
|
|
703
|
+
paintSwitch();
|
|
485
704
|
if (n === 0) {
|
|
486
705
|
box.className = "agempty";
|
|
706
|
+
delete box.dataset.layout;
|
|
487
707
|
box.innerHTML = `<div>${esc(t("ag.empty"))}</div>`;
|
|
488
708
|
return;
|
|
489
709
|
}
|
|
490
|
-
|
|
491
|
-
const
|
|
710
|
+
const layout = shownLayout();
|
|
711
|
+
const shown = visibleIds();
|
|
712
|
+
box.className = `agp n${Math.max(1, shown.length)}${layout === "tabs" ? " tabs" : ""}`;
|
|
713
|
+
box.dataset.layout = layout;
|
|
714
|
+
const empty = box.querySelector(":scope > div:not(.pane):not(.agtabs)");
|
|
492
715
|
if (empty) empty.remove();
|
|
493
|
-
|
|
716
|
+
let strip = box.querySelector(":scope > .agtabs");
|
|
717
|
+
if (layout === "tabs") {
|
|
718
|
+
if (!strip) {
|
|
719
|
+
strip = document.createElement("div");
|
|
720
|
+
strip.className = "agtabs";
|
|
721
|
+
strip.setAttribute("role", "tablist");
|
|
722
|
+
strip.setAttribute("aria-label", t("ag.tabs"));
|
|
723
|
+
}
|
|
724
|
+
box.prepend(strip);
|
|
725
|
+
paintTabs(strip);
|
|
726
|
+
// The tab in front is in sight in the strip (it scrolls sideways;
|
|
727
|
+
// the page itself never scrolls for it).
|
|
728
|
+
const on = strip.querySelector('[role="tab"][aria-selected="true"]');
|
|
729
|
+
const w = on?.parentElement;
|
|
730
|
+
if (w && strip.clientWidth > 0) {
|
|
731
|
+
const left = w.offsetLeft - strip.offsetLeft;
|
|
732
|
+
if (left < strip.scrollLeft) strip.scrollLeft = left;
|
|
733
|
+
else if (left + w.offsetWidth > strip.scrollLeft + strip.clientWidth)
|
|
734
|
+
strip.scrollLeft = left + w.offsetWidth - strip.clientWidth;
|
|
735
|
+
}
|
|
736
|
+
} else strip?.remove();
|
|
737
|
+
// Panes in the URL's order; those out of sight stay attached, hidden.
|
|
494
738
|
for (const id of st.open) {
|
|
495
739
|
const p = st.panes.get(id);
|
|
496
|
-
if (p)
|
|
740
|
+
if (!p) continue;
|
|
741
|
+
p.el.hidden = !shown.includes(id);
|
|
742
|
+
p.el.classList.toggle("in-tabs", layout === "tabs");
|
|
743
|
+
const x = p.el.querySelector('.bar2 [data-act="close"]');
|
|
744
|
+
if (x) x.hidden = layout === "tabs";
|
|
745
|
+
box.appendChild(p.el);
|
|
746
|
+
}
|
|
747
|
+
// Each pane in sight fits its new box (a hidden one is never measured).
|
|
748
|
+
requestAnimationFrame(() => {
|
|
749
|
+
for (const id of shown) {
|
|
750
|
+
const p = st.panes.get(id);
|
|
751
|
+
if (p) refit(p);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** The header's switch: the person's layout pressed (AG-4b). */
|
|
757
|
+
function paintSwitch() {
|
|
758
|
+
for (const b of st.root.querySelectorAll("[data-layouts] button"))
|
|
759
|
+
b.setAttribute("aria-pressed", String(b.dataset.layout === st.layout));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Switch the layout (the header's switch, ⌘1-3): drawn at once, the URL
|
|
764
|
+
* follows, and it is saved for the person. Focus shows the pane in front.
|
|
765
|
+
*/
|
|
766
|
+
function setLayout(layout) {
|
|
767
|
+
if (!LAYOUTS.includes(layout)) return;
|
|
768
|
+
const was = st.layout;
|
|
769
|
+
st.layout = layout;
|
|
770
|
+
st.side.clear();
|
|
771
|
+
paintLayout();
|
|
772
|
+
paintList();
|
|
773
|
+
writeUrl();
|
|
774
|
+
if (layout === was) return;
|
|
775
|
+
api("POST", "/api/settings", { agents_layout: layout }).catch((err) =>
|
|
776
|
+
window.denShell?.errorToast?.(err),
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Open (or bring forward) the session of a row the person clicked, as the
|
|
782
|
+
* layout says: Focus replaces the pane (`add`: Shift- or ⌘-click, one
|
|
783
|
+
* alongside), Tabs opens a tab, Tile adds a tile.
|
|
784
|
+
*/
|
|
785
|
+
async function pick(id, add) {
|
|
786
|
+
const layout = shownLayout();
|
|
787
|
+
if (layout === "focus" && !add) {
|
|
788
|
+
st.side.clear();
|
|
789
|
+
for (const other of [...st.open])
|
|
790
|
+
if (other !== id) closePane(other, { quiet: true });
|
|
791
|
+
} else if (layout === "focus" && add && st.cur && st.cur !== id)
|
|
792
|
+
st.side.add(id);
|
|
793
|
+
if (!(layout === "focus" && add)) st.cur = id;
|
|
794
|
+
if (st.panes.has(id)) {
|
|
795
|
+
paintLayout();
|
|
796
|
+
paintList();
|
|
797
|
+
st.panes.get(id).term?.focus();
|
|
798
|
+
return;
|
|
497
799
|
}
|
|
800
|
+
await openPane(id, { front: !(layout === "focus" && add) });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Double-click a tab: its name becomes an input (Enter saves, Esc doesn't). */
|
|
804
|
+
function renameTab(tab) {
|
|
805
|
+
const id = tab.dataset.id;
|
|
806
|
+
const r = rowOf(id);
|
|
807
|
+
if (r?.kind !== "agent" || tab.querySelector("input")) return;
|
|
808
|
+
const input = document.createElement("input");
|
|
809
|
+
input.type = "text";
|
|
810
|
+
input.className = "agtab-in";
|
|
811
|
+
input.maxLength = MAX_NAME;
|
|
812
|
+
input.value = r.displayName || r.agent;
|
|
813
|
+
input.setAttribute("aria-label", t("ag.renameName", { id: r.agent }));
|
|
814
|
+
// Beside the tab, not in it (a button holds no input): the tab hides.
|
|
815
|
+
tab.hidden = true;
|
|
816
|
+
tab.before(input);
|
|
817
|
+
input.focus();
|
|
818
|
+
input.select();
|
|
819
|
+
let done = false;
|
|
820
|
+
const finish = async (save) => {
|
|
821
|
+
if (done) return;
|
|
822
|
+
done = true;
|
|
823
|
+
const name = input.value.trim();
|
|
824
|
+
input.remove();
|
|
825
|
+
tab.hidden = false;
|
|
826
|
+
if (save && name !== (r.displayName || r.agent)) {
|
|
827
|
+
try {
|
|
828
|
+
await api("POST", agentPath(r, "name"), {
|
|
829
|
+
name: name && name !== r.agent ? name : null,
|
|
830
|
+
});
|
|
831
|
+
} catch (err) {
|
|
832
|
+
window.denShell?.errorToast?.(err);
|
|
833
|
+
}
|
|
834
|
+
await refresh();
|
|
835
|
+
}
|
|
836
|
+
paintLayout();
|
|
837
|
+
};
|
|
838
|
+
input.addEventListener("keydown", (e) => {
|
|
839
|
+
e.stopPropagation();
|
|
840
|
+
if (e.key === "Enter") {
|
|
841
|
+
e.preventDefault();
|
|
842
|
+
void finish(true);
|
|
843
|
+
} else if (e.key === "Escape") {
|
|
844
|
+
e.preventDefault();
|
|
845
|
+
void finish(false);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
input.addEventListener("blur", () => void finish(true));
|
|
849
|
+
// A click in the input is the input's, not the tab's.
|
|
850
|
+
input.addEventListener("click", (e) => e.stopPropagation());
|
|
498
851
|
}
|
|
499
852
|
|
|
500
853
|
function paintBar(p) {
|
|
501
854
|
const r = rowOf(p.id);
|
|
502
855
|
const bar = p.el.querySelector(".bar2");
|
|
503
856
|
const name = r ? paneName(r) : p.label || p.id;
|
|
857
|
+
// AG-4b: the session's name last, when it says more.
|
|
504
858
|
const where = r
|
|
505
|
-
? [r.workspace, r.runtime].filter(Boolean).join(" · ")
|
|
859
|
+
? [r.workspace, r.runtime, sessionExtra(r)].filter(Boolean).join(" · ")
|
|
506
860
|
: p.workspace || "";
|
|
507
861
|
const tone = r ? stateTone(r) : p.exited ? "off" : "pending";
|
|
508
862
|
const stop =
|
|
@@ -524,6 +878,9 @@ function paintBar(p) {
|
|
|
524
878
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
525
879
|
: "") +
|
|
526
880
|
`<span class="x">${reconnect}${stop}${resume}<button type="button" class="den-btn sm" data-act="close" aria-label="${esc(t("ag.detachName", { id: name }))}" title="${esc(t("ag.detach"))}">×</button></span>`;
|
|
881
|
+
// Tabs: the tab's × closes it; the pane's bar has none.
|
|
882
|
+
const x = bar.querySelector('[data-act="close"]');
|
|
883
|
+
if (x) x.hidden = shownLayout() === "tabs";
|
|
527
884
|
}
|
|
528
885
|
|
|
529
886
|
function send(p, obj) {
|
|
@@ -556,11 +913,26 @@ function refit(p) {
|
|
|
556
913
|
markSize(p);
|
|
557
914
|
}
|
|
558
915
|
|
|
559
|
-
/**
|
|
916
|
+
/**
|
|
917
|
+
* The pane's size and the PTY's, on the pane (data-cols, data-pty-cols),
|
|
918
|
+
* and the cell size xterm drew with (data-cell-w, data-cell-h: its
|
|
919
|
+
* renderer's CSS cell, sub-pixel), so a check can tell a missed row from
|
|
920
|
+
* a fit's remainder.
|
|
921
|
+
*/
|
|
560
922
|
function markSize(p) {
|
|
561
923
|
if (!p.term) return;
|
|
562
924
|
p.el.dataset.cols = String(p.term.cols);
|
|
563
925
|
p.el.dataset.rows = String(p.term.rows);
|
|
926
|
+
let cell = null;
|
|
927
|
+
try {
|
|
928
|
+
cell = p.term._core?._renderService?.dimensions?.css?.cell ?? null;
|
|
929
|
+
} catch {
|
|
930
|
+
cell = null;
|
|
931
|
+
}
|
|
932
|
+
if (cell?.width > 0 && cell?.height > 0) {
|
|
933
|
+
p.el.dataset.cellW = String(cell.width);
|
|
934
|
+
p.el.dataset.cellH = String(cell.height);
|
|
935
|
+
}
|
|
564
936
|
if (p.pty) {
|
|
565
937
|
p.el.dataset.ptyCols = String(p.pty.cols);
|
|
566
938
|
p.el.dataset.ptyRows = String(p.pty.rows);
|
|
@@ -695,11 +1067,111 @@ async function connect(p) {
|
|
|
695
1067
|
};
|
|
696
1068
|
}
|
|
697
1069
|
|
|
698
|
-
|
|
1070
|
+
// ── links and keys in a pane (AG-4b) ───────────────────────────────────
|
|
1071
|
+
|
|
1072
|
+
/** What Shift+Enter sends to a pane's session (a plain session: Enter). */
|
|
1073
|
+
function softNewlineOf(id) {
|
|
1074
|
+
const seq = rowOf(id)?.softNewline;
|
|
1075
|
+
return typeof seq === "string" && seq ? seq : "\r";
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* A URL or an OSC 8 link clicked: with ⌘ (Ctrl elsewhere), an http or
|
|
1080
|
+
* https one opens in a new tab, noopener. A plain click opens nothing
|
|
1081
|
+
* (it selects); every other scheme (javascript:, data:, file:) nothing.
|
|
1082
|
+
*/
|
|
1083
|
+
function openWeb(e, uri) {
|
|
1084
|
+
if (!linkModifier(e) || !isWebUrl(uri)) return;
|
|
1085
|
+
e?.preventDefault?.();
|
|
1086
|
+
window.open(uri, "_blank", "noopener");
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** The most paths asked about at once (agent-paths.ts MAX_PATHS). */
|
|
1090
|
+
const MAX_ASK = 64;
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Which of `paths` are links for agent row `r`: absolute path or null,
|
|
1094
|
+
* each asked once per pane (GET …/agents/<id>/paths: the server resolves
|
|
1095
|
+
* them against its cwd, inside its workspace; stat only).
|
|
1096
|
+
*/
|
|
1097
|
+
async function pathLinks(p, r, paths) {
|
|
1098
|
+
p.paths ??= new Map();
|
|
1099
|
+
const ask = [...new Set(paths)].filter((x) => !p.paths.has(x));
|
|
1100
|
+
for (let i = 0; i < ask.length; i += MAX_ASK) {
|
|
1101
|
+
const batch = ask.slice(i, i + MAX_ASK);
|
|
1102
|
+
const q = batch.map((x) => `p=${encodeURIComponent(x)}`).join("&");
|
|
1103
|
+
const got = fetch(`${agentPath(r, "paths")}?${q}`, {
|
|
1104
|
+
headers: { accept: "json" },
|
|
1105
|
+
})
|
|
1106
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1107
|
+
.catch(() => null);
|
|
1108
|
+
for (const x of batch)
|
|
1109
|
+
p.paths.set(
|
|
1110
|
+
x,
|
|
1111
|
+
got.then((b) => {
|
|
1112
|
+
const v = b?.paths?.[x];
|
|
1113
|
+
return typeof v === "string" ? v : null;
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
const out = new Map();
|
|
1118
|
+
for (const x of paths) out.set(x, await p.paths.get(x));
|
|
1119
|
+
return out;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/** xterm asks for line `y`'s links (1-based): its paths that exist. */
|
|
1123
|
+
async function providePathLinks(p, y, done) {
|
|
1124
|
+
try {
|
|
1125
|
+
const r = rowOf(p.id);
|
|
1126
|
+
const line = p.term?.buffer.active.getLine(y - 1)?.translateToString(true);
|
|
1127
|
+
if (r?.kind !== "agent" || !r.agent || !line) return done(undefined);
|
|
1128
|
+
const found = findPaths(line);
|
|
1129
|
+
if (!found.length) return done(undefined);
|
|
1130
|
+
const abs = await pathLinks(
|
|
1131
|
+
p,
|
|
1132
|
+
r,
|
|
1133
|
+
found.map((m) => m.path),
|
|
1134
|
+
);
|
|
1135
|
+
const links = found
|
|
1136
|
+
.filter((m) => abs.get(m.path))
|
|
1137
|
+
.map((m) => ({
|
|
1138
|
+
text: m.text,
|
|
1139
|
+
range: { start: { x: m.start + 1, y }, end: { x: m.end, y } },
|
|
1140
|
+
decorations: { pointerCursor: true, underline: true },
|
|
1141
|
+
activate: (e) => void openPath(e, abs.get(m.path), m.line, m.col),
|
|
1142
|
+
}));
|
|
1143
|
+
done(links.length ? links : undefined);
|
|
1144
|
+
} catch {
|
|
1145
|
+
done(undefined);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* A path link clicked: with ⌘ (Ctrl elsewhere), the editor in Settings
|
|
1151
|
+
* (editor_open) at the line, by its URL scheme; none copies the path.
|
|
1152
|
+
*/
|
|
1153
|
+
async function openPath(e, abs, line, col) {
|
|
1154
|
+
if (!linkModifier(e) || !abs) return;
|
|
1155
|
+
e?.preventDefault?.();
|
|
1156
|
+
const url = editorUrl(st.editorOpen, abs, line, col);
|
|
1157
|
+
if (url) {
|
|
1158
|
+
window.open(url, "_self");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
await navigator.clipboard.writeText(abs);
|
|
1163
|
+
window.denShell?.toast?.(t("ag.copied"));
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
window.denShell?.errorToast?.(err);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function openPane(id, { front = true } = {}) {
|
|
699
1170
|
if (st.panes.has(id)) {
|
|
700
1171
|
st.panes.get(id).term?.focus();
|
|
701
1172
|
return;
|
|
702
1173
|
}
|
|
1174
|
+
if (front) st.cur = id;
|
|
703
1175
|
if (st.open.length >= MAX_PANES) {
|
|
704
1176
|
const oldest = st.open[0];
|
|
705
1177
|
window.denShell?.toast?.(
|
|
@@ -710,7 +1182,10 @@ async function openPane(id) {
|
|
|
710
1182
|
const el = document.createElement("section");
|
|
711
1183
|
el.className = "pane";
|
|
712
1184
|
el.dataset.id = id;
|
|
713
|
-
|
|
1185
|
+
// xterm opens in .tbox, the .term's content box: the fit measures its
|
|
1186
|
+
// parent's height and width as they are, so the .term's padding must
|
|
1187
|
+
// not be in it (AG-4b: every terminal fits its pane).
|
|
1188
|
+
el.innerHTML = `<div class="bar2"></div><div class="term"><div class="tbox"></div></div>`;
|
|
714
1189
|
const p = {
|
|
715
1190
|
id,
|
|
716
1191
|
el,
|
|
@@ -745,18 +1220,50 @@ async function openPane(id) {
|
|
|
745
1220
|
scrollback: 5000,
|
|
746
1221
|
theme: termTheme(),
|
|
747
1222
|
allowProposedApi: false,
|
|
1223
|
+
// OSC 8 links (AG-4b): ⌘/Ctrl-click, http and https only, no confirm.
|
|
1224
|
+
linkHandler: {
|
|
1225
|
+
activate: (e, uri) => void openWeb(e, uri),
|
|
1226
|
+
allowNonHttpProtocols: false,
|
|
1227
|
+
},
|
|
748
1228
|
});
|
|
749
1229
|
const fit = new lib.FitAddon();
|
|
750
1230
|
term.loadAddon(fit);
|
|
751
|
-
|
|
1231
|
+
// Plain URLs (AG-4b): the same rule as OSC 8.
|
|
1232
|
+
term.loadAddon(new lib.WebLinksAddon((e, uri) => void openWeb(e, uri)));
|
|
1233
|
+
term.open(el.querySelector(".term .tbox"));
|
|
752
1234
|
p.term = term;
|
|
753
1235
|
p.fit = fit;
|
|
754
1236
|
const enc = new TextEncoder();
|
|
755
|
-
|
|
1237
|
+
const typeIn = (d) => {
|
|
756
1238
|
if (p.sock?.readyState !== WebSocket.OPEN) return;
|
|
757
1239
|
// The one typing wins: another viewer resized the PTY since.
|
|
758
1240
|
if (outOfStep(p)) claimSize(p);
|
|
759
1241
|
p.sock.send(enc.encode(d));
|
|
1242
|
+
};
|
|
1243
|
+
term.onData(typeIn);
|
|
1244
|
+
// AG-4b: Shift+Enter is the runtime's soft newline, never a CR; the
|
|
1245
|
+
// layout keys (⌃⇧1-3) are the page's, never the agent's.
|
|
1246
|
+
term.attachCustomKeyEventHandler((e) => {
|
|
1247
|
+
if (e.ctrlKey && e.shiftKey && /^Digit[123]$/.test(e.code ?? ""))
|
|
1248
|
+
return false;
|
|
1249
|
+
if (
|
|
1250
|
+
e.key === "Enter" &&
|
|
1251
|
+
e.shiftKey &&
|
|
1252
|
+
!e.ctrlKey &&
|
|
1253
|
+
!e.altKey &&
|
|
1254
|
+
!e.metaKey
|
|
1255
|
+
) {
|
|
1256
|
+
if (e.isComposing) return true;
|
|
1257
|
+
e.preventDefault();
|
|
1258
|
+
if (softNewlineKey(e)) typeIn(softNewlineOf(id));
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
return true;
|
|
1262
|
+
});
|
|
1263
|
+
// File paths (AG-4b): links only where the server says they exist in
|
|
1264
|
+
// the agent's workspace.
|
|
1265
|
+
term.registerLinkProvider({
|
|
1266
|
+
provideLinks: (y, done) => void providePathLinks(p, y, done),
|
|
760
1267
|
});
|
|
761
1268
|
term.onResize(() => sendSize(p));
|
|
762
1269
|
// Focused (a click, the window coming back): this pane's size.
|
|
@@ -777,6 +1284,8 @@ function closePane(id, { quiet = false } = {}) {
|
|
|
777
1284
|
p.closed = true;
|
|
778
1285
|
st.panes.delete(id);
|
|
779
1286
|
st.open = st.open.filter((x) => x !== id);
|
|
1287
|
+
st.side.delete(id);
|
|
1288
|
+
if (st.cur === id) st.cur = st.open.at(-1) ?? null;
|
|
780
1289
|
try {
|
|
781
1290
|
p.sock?.close(1000, "closed");
|
|
782
1291
|
} catch {
|
|
@@ -984,7 +1493,7 @@ function review() {
|
|
|
984
1493
|
)?.focus();
|
|
985
1494
|
}
|
|
986
1495
|
|
|
987
|
-
// ── Start agent
|
|
1496
|
+
// ── Start agent (AG-10.1) ───────────────────────────────────────────────
|
|
988
1497
|
|
|
989
1498
|
// Personas and runtimes, per workspace (AG-0.5: a workspace's own runtime
|
|
990
1499
|
// paths win), asked once per page each.
|
|
@@ -1006,79 +1515,478 @@ function personasFor(ws) {
|
|
|
1006
1515
|
return personasAsked.get(key);
|
|
1007
1516
|
}
|
|
1008
1517
|
|
|
1009
|
-
/**
|
|
1518
|
+
/**
|
|
1519
|
+
* "Plain session": no persona, a shell in the workspace (AG-10.1). A ":"
|
|
1520
|
+
* is never in a persona's id.
|
|
1521
|
+
*/
|
|
1522
|
+
const PLAIN = ":plain";
|
|
1523
|
+
|
|
1524
|
+
/** The dialog's state while it is open. */
|
|
1525
|
+
const sd = {
|
|
1526
|
+
data: null,
|
|
1527
|
+
f: { q: "", cat: "", src: "" },
|
|
1528
|
+
pid: "",
|
|
1529
|
+
det: false,
|
|
1530
|
+
what: false,
|
|
1531
|
+
detail: null,
|
|
1532
|
+
// Advanced: "" = the persona's.
|
|
1533
|
+
model: "",
|
|
1534
|
+
ver: "",
|
|
1535
|
+
intent: "ask",
|
|
1536
|
+
extras: [],
|
|
1537
|
+
extrasOpen: false,
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
const $d = (sel) => st.root.querySelector(sel);
|
|
1541
|
+
const curPersona = () =>
|
|
1542
|
+
sd.pid === PLAIN
|
|
1543
|
+
? null
|
|
1544
|
+
: (sd.data?.personas ?? []).find((p) => p.id === sd.pid) || null;
|
|
1545
|
+
|
|
1546
|
+
/** The runtime the dialog will start: the one picked, else the persona's. */
|
|
1547
|
+
function chosenRuntime() {
|
|
1548
|
+
const p = curPersona();
|
|
1549
|
+
return $d("#ag-runtime").value || personaRuntime(sd.data, p);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/** The persona's own model and version, on the runtime chosen (AG-12.3). */
|
|
1553
|
+
function personaModel() {
|
|
1554
|
+
const p = curPersona();
|
|
1555
|
+
const same = p && personaRuntime(sd.data, p) === chosenRuntime();
|
|
1556
|
+
return {
|
|
1557
|
+
model: same ? p.model || "" : "",
|
|
1558
|
+
version: same ? p.version || "latest" : "latest",
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/** The persona list's rows, and Plain session pinned at the bottom. */
|
|
1563
|
+
function plistHtml() {
|
|
1564
|
+
const ps = sd.data?.personas ?? [];
|
|
1565
|
+
const cats = sd.data?.categories ?? [];
|
|
1566
|
+
return (
|
|
1567
|
+
personaListHtml(sd.f, ps, cats, (p) =>
|
|
1568
|
+
personaRowHtml(p, p.id === sd.pid, sd.data),
|
|
1569
|
+
) +
|
|
1570
|
+
`<div class="pgh"><span>${esc(t("ag.session"))}</span><span></span></div>` +
|
|
1571
|
+
`<button type="button" class="pr" role="radio" data-pid="${esc(PLAIN)}" aria-checked="${sd.pid === PLAIN}"><span class="rd" aria-hidden="true"></span><span class="pn">${esc(t("ag.new.plain"))}</span><span></span><span class="pd">${esc(t("ag.new.plainP"))}</span></button>`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function paintPlist(keepScroll = true) {
|
|
1576
|
+
const l = $d("#ag-persona");
|
|
1577
|
+
const top = l.scrollTop;
|
|
1578
|
+
l.innerHTML = plistHtml();
|
|
1579
|
+
l.scrollTop = keepScroll ? top : 0;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/** The Details row (and Duplicate or Edit) under the list, and Details. */
|
|
1583
|
+
function paintDetails() {
|
|
1584
|
+
const p = curPersona();
|
|
1585
|
+
const el = $d("#ag-detrow");
|
|
1586
|
+
if (!p) {
|
|
1587
|
+
el.innerHTML = "";
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
const mine = p.origin === "local";
|
|
1591
|
+
el.innerHTML =
|
|
1592
|
+
`<div class="fldh"><button type="button" class="lnk" data-x="det" aria-expanded="${sd.det}" aria-controls="ag-det">${esc(sd.det ? t("ag.new.hide") : t("ag.new.details", { n: personaName(p) }))}</button><span class="sp"></span>` +
|
|
1593
|
+
(mine
|
|
1594
|
+
? `<button type="button" class="lnk" data-x="edit">${esc(t("pe.editB"))}</button>`
|
|
1595
|
+
: "") +
|
|
1596
|
+
`<button type="button" class="lnk" data-x="dup">${esc(t("pe.dup"))}</button></div>` +
|
|
1597
|
+
(sd.det
|
|
1598
|
+
? personaDetailHtml(
|
|
1599
|
+
sd.detail?.id === p.id ? sd.detail : { ...p, instructions: null },
|
|
1600
|
+
sd.data,
|
|
1601
|
+
)
|
|
1602
|
+
: "");
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* The runtime select and its readiness line (AG-0.2), and AG-12.2: a
|
|
1607
|
+
* persona whose runtime isn't installed starts on the first one that is,
|
|
1608
|
+
* and the dialog says so.
|
|
1609
|
+
*/
|
|
1610
|
+
function paintRuntime(reset) {
|
|
1611
|
+
const rs = $d("#ag-runtime");
|
|
1612
|
+
const go = $d("#ag-go");
|
|
1613
|
+
const ready = $d("#ag-r-ready");
|
|
1614
|
+
const plain = sd.pid === PLAIN;
|
|
1615
|
+
$d("#ag-r-l").hidden = plain;
|
|
1616
|
+
$d("#ag-task-l").hidden = plain;
|
|
1617
|
+
$d("#ag-adv").hidden = plain;
|
|
1618
|
+
const fb = $d("#ag-r-fb");
|
|
1619
|
+
fb.hidden = true;
|
|
1620
|
+
if (plain) {
|
|
1621
|
+
$d("#ag-r-miss").hidden = true;
|
|
1622
|
+
ready.hidden = true;
|
|
1623
|
+
go.disabled = false;
|
|
1624
|
+
go.dataset.blocked = "";
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const personas = sd.data?.personas ?? [];
|
|
1628
|
+
const runtimes = sd.data?.runtimes ?? [];
|
|
1629
|
+
const keep = reset ? "" : rs.value;
|
|
1630
|
+
rs.innerHTML = runtimeOptions(personas, runtimes, sd.pid, t);
|
|
1631
|
+
if ([...rs.options].some((o) => o.value === keep && !o.disabled))
|
|
1632
|
+
rs.value = keep;
|
|
1633
|
+
const p = curPersona();
|
|
1634
|
+
if (reset && p && !runtimeInstalled(sd.data, p)) {
|
|
1635
|
+
const first = [...rs.options].find((o) => o.value && !o.disabled);
|
|
1636
|
+
if (first) {
|
|
1637
|
+
rs.value = first.value;
|
|
1638
|
+
fb.textContent = t("rt.fallback", {
|
|
1639
|
+
p: personaName(p),
|
|
1640
|
+
r: runtimeName(runtimes, personaRuntime(sd.data, p)),
|
|
1641
|
+
f: runtimeName(runtimes, first.value),
|
|
1642
|
+
});
|
|
1643
|
+
fb.hidden = false;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
// AG-12.1: what isn't installed here, and Install… (later).
|
|
1647
|
+
const miss = runtimes.filter(
|
|
1648
|
+
(x) => !x.available && x.denTools !== undefined && x.source !== "setting",
|
|
1649
|
+
);
|
|
1650
|
+
const ml = $d("#ag-r-miss");
|
|
1651
|
+
ml.innerHTML = miss.length
|
|
1652
|
+
? `${esc(t("rt.missingLine"))} ${miss.map((x) => `<span class="mono">${esc(x.label || x.id)}</span>`).join(", ")} · <span class="lnk" aria-disabled="true" title="${esc(t("rt.later"))}">${esc(t("rt.install"))}</span> <span class="tagx">${esc(t("rt.later"))}</span>`
|
|
1653
|
+
: "";
|
|
1654
|
+
ml.hidden = !miss.length;
|
|
1655
|
+
const r = readiness(sd.data, sd.pid, rs.value, t);
|
|
1656
|
+
ready.innerHTML = r.html;
|
|
1657
|
+
ready.hidden = !r.html;
|
|
1658
|
+
ready.dataset.tone = r.tone;
|
|
1659
|
+
go.disabled = !r.ok;
|
|
1660
|
+
go.dataset.blocked = r.ok ? "" : "runtime";
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/** Advanced (AG-12, AG-13): model, version and the permission block. */
|
|
1664
|
+
function paintAdvanced() {
|
|
1665
|
+
const p = curPersona();
|
|
1666
|
+
const body = $d("#ag-adv-b");
|
|
1667
|
+
if (!p) {
|
|
1668
|
+
body.innerHTML = "";
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const rt = chosenRuntime();
|
|
1672
|
+
const rtv = (sd.data?.runtimes ?? []).find((r) => r.id === rt);
|
|
1673
|
+
const name = runtimeName(sd.data?.runtimes ?? [], rt);
|
|
1674
|
+
const def = personaModel();
|
|
1675
|
+
const models = rtv?.models ?? [];
|
|
1676
|
+
const model = sd.model || def.model;
|
|
1677
|
+
const first = t("ag.new.modelDef", {
|
|
1678
|
+
m: `${def.model || t("pe.rtDefault")} · ${versionLabel(def.model ? def.version : "latest")}`,
|
|
1679
|
+
});
|
|
1680
|
+
const verFirst = t("ag.new.verDef", {
|
|
1681
|
+
v: versionLabel(
|
|
1682
|
+
sd.model && sd.model !== def.model ? "latest" : def.version,
|
|
1683
|
+
),
|
|
1684
|
+
});
|
|
1685
|
+
body.innerHTML =
|
|
1686
|
+
`<p>${esc(t("ag.new.advNote"))}</p>` +
|
|
1687
|
+
`<div class="two"><label class="fld"><span>${esc(t("ag.new.model"))}</span><select id="ag-model"${models.length ? "" : " disabled"}>${models.length ? modelOptionsHtml(sd.data, rt, sd.model, first) : `<option value="">${esc(t("pe.nsModel", { r: name }))}</option>`}</select></label>` +
|
|
1688
|
+
`<label class="fld"><span>${esc(t("ag.new.ver"))}</span><select id="ag-ver"${models.length && model ? "" : " disabled"}>${versionOptionsHtml(sd.data, rt, model, sd.ver, verFirst)}</select></label></div>` +
|
|
1689
|
+
permBlockHtml("ag", sd.data, {
|
|
1690
|
+
intent: sd.intent,
|
|
1691
|
+
role: p.role,
|
|
1692
|
+
extras: sd.extras,
|
|
1693
|
+
runtime: rt,
|
|
1694
|
+
extrasOpen: sd.extrasOpen,
|
|
1695
|
+
disabled: !rtv?.permissions,
|
|
1696
|
+
});
|
|
1697
|
+
$d("#ag-tools").disabled = false;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/** Pick persona `id` (or PLAIN): its own choices fill Advanced in. */
|
|
1701
|
+
function pickPersona(id) {
|
|
1702
|
+
sd.pid = id;
|
|
1703
|
+
sd.det = sd.det && id !== PLAIN;
|
|
1704
|
+
const p = curPersona();
|
|
1705
|
+
sd.model = "";
|
|
1706
|
+
sd.ver = "";
|
|
1707
|
+
sd.intent = p?.permission || "ask";
|
|
1708
|
+
sd.extras = [...(p?.allowedCommands ?? [])];
|
|
1709
|
+
sd.extrasOpen = sd.extras.length > 0;
|
|
1710
|
+
for (const b of st.root.querySelectorAll("#ag-persona [data-pid]"))
|
|
1711
|
+
b.setAttribute("aria-checked", String(b.dataset.pid === id));
|
|
1712
|
+
paintDetails();
|
|
1713
|
+
paintRuntime(true);
|
|
1714
|
+
paintAdvanced();
|
|
1715
|
+
if (sd.det) void loadDetail();
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async function loadDetail() {
|
|
1719
|
+
const p = curPersona();
|
|
1720
|
+
if (!p) return;
|
|
1721
|
+
try {
|
|
1722
|
+
const d = await personaDetail(p.id);
|
|
1723
|
+
if (curPersona()?.id !== d.id) return;
|
|
1724
|
+
sd.detail = d;
|
|
1725
|
+
paintDetails();
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
window.denShell?.errorToast?.(err);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/** The whole persona field, for the data asked. */
|
|
1732
|
+
function paintPersonaField() {
|
|
1733
|
+
const ps = sd.data?.personas ?? [];
|
|
1734
|
+
$d("#ag-pfbar").innerHTML = filterBarHtml(
|
|
1735
|
+
sd.f,
|
|
1736
|
+
"ag",
|
|
1737
|
+
ps,
|
|
1738
|
+
sd.data?.categories ?? [],
|
|
1739
|
+
);
|
|
1740
|
+
$d("#ag-what").hidden = !sd.what;
|
|
1741
|
+
paintPlist(false);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/** Ask for the workspace's personas and runtimes; false when it failed. */
|
|
1010
1745
|
async function fillForWorkspace(ws) {
|
|
1011
|
-
let data;
|
|
1012
1746
|
try {
|
|
1013
|
-
data = await personasFor(ws);
|
|
1747
|
+
sd.data = normalizeData(await personasFor(ws));
|
|
1014
1748
|
} catch (err) {
|
|
1015
1749
|
window.denShell?.errorToast?.(err);
|
|
1016
1750
|
return false;
|
|
1017
1751
|
}
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
ps.onchange?.();
|
|
1029
|
-
}
|
|
1030
|
-
const rs = form.querySelector("#ag-runtime");
|
|
1031
|
-
if ([...rs.options].some((o) => o.value === keep[1] && !o.disabled)) {
|
|
1032
|
-
rs.value = keep[1];
|
|
1033
|
-
rs.onchange?.();
|
|
1752
|
+
const ps = sd.data?.personas ?? [];
|
|
1753
|
+
if (sd.pid !== PLAIN && !ps.some((p) => p.id === sd.pid)) {
|
|
1754
|
+
// The first persona the list shows (grouped by category).
|
|
1755
|
+
const cats = (sd.data?.categories ?? []).map((c) => c.id);
|
|
1756
|
+
const sorted = [...ps].sort(
|
|
1757
|
+
(a, b) =>
|
|
1758
|
+
(cats.indexOf(a.category) + 1 || 99) -
|
|
1759
|
+
(cats.indexOf(b.category) + 1 || 99),
|
|
1760
|
+
);
|
|
1761
|
+
sd.pid = sorted[0]?.id ?? PLAIN;
|
|
1034
1762
|
}
|
|
1763
|
+
paintPersonaField();
|
|
1764
|
+
pickPersona(sd.pid);
|
|
1035
1765
|
return true;
|
|
1036
1766
|
}
|
|
1037
1767
|
|
|
1038
1768
|
async function openStart() {
|
|
1039
|
-
const dlg =
|
|
1769
|
+
const dlg = $d("#ag-dlg");
|
|
1040
1770
|
if (!dlg || dlg.open) return;
|
|
1041
1771
|
const shown = viewWorkspaces();
|
|
1042
|
-
const wsSel =
|
|
1772
|
+
const wsSel = $d("#ag-ws");
|
|
1043
1773
|
wsSel.innerHTML = shown
|
|
1044
1774
|
.map((w) => `<option value="${esc(w)}">${esc(w)}</option>`)
|
|
1045
1775
|
.join("");
|
|
1046
|
-
|
|
1776
|
+
wsSel.disabled = shown.length <= 1;
|
|
1777
|
+
sd.f = { q: "", cat: "", src: "" };
|
|
1778
|
+
sd.det = false;
|
|
1779
|
+
sd.what = false;
|
|
1780
|
+
$d("#ag-err").hidden = true;
|
|
1781
|
+
$d("#ag-task").value = "";
|
|
1782
|
+
$d("#ag-adv").open = false;
|
|
1047
1783
|
if (!(await fillForWorkspace(wsSel.value || shown[0]))) return;
|
|
1048
1784
|
if (dlg.open) return;
|
|
1049
1785
|
if (typeof dlg.showModal === "function") dlg.showModal();
|
|
1050
1786
|
else dlg.setAttribute("open", "");
|
|
1787
|
+
$d("#ag-q")?.focus();
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/** What Start sends (agent-routes.ts checks each field again). */
|
|
1791
|
+
function startBody() {
|
|
1792
|
+
const p = curPersona();
|
|
1793
|
+
const rt = chosenRuntime();
|
|
1794
|
+
const rtv = (sd.data?.runtimes ?? []).find((r) => r.id === rt);
|
|
1795
|
+
const runtime = $d("#ag-runtime").value;
|
|
1796
|
+
const body = { persona: p.id, ...(runtime ? { runtime } : {}) };
|
|
1797
|
+
const def = personaModel();
|
|
1798
|
+
if (rtv?.models?.length) {
|
|
1799
|
+
if (sd.model) body.model = sd.model;
|
|
1800
|
+
if (sd.ver) {
|
|
1801
|
+
body.version = sd.ver;
|
|
1802
|
+
body.model ??= def.model || undefined;
|
|
1803
|
+
if (!body.model) delete body.version;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
if (rtv?.permissions) {
|
|
1807
|
+
// A persona's own mode Den doesn't name (Claude Code's plan) stays its
|
|
1808
|
+
// own unless a mode is picked.
|
|
1809
|
+
if (p.permission !== undefined || sd.intent !== "ask")
|
|
1810
|
+
body.permission = sd.intent;
|
|
1811
|
+
body.allow = sd.extras;
|
|
1812
|
+
}
|
|
1813
|
+
const task = $d("#ag-task").value.trim();
|
|
1814
|
+
if (task) body.instruction = task;
|
|
1815
|
+
return body;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function showStartError(err) {
|
|
1819
|
+
const el = $d("#ag-err");
|
|
1820
|
+
el.textContent = err?.message || String(err);
|
|
1821
|
+
el.hidden = false;
|
|
1051
1822
|
}
|
|
1052
1823
|
|
|
1053
1824
|
async function submitStart(form) {
|
|
1054
|
-
const dlg =
|
|
1055
|
-
const go =
|
|
1825
|
+
const dlg = $d("#ag-dlg");
|
|
1826
|
+
const go = $d("#ag-go");
|
|
1056
1827
|
if (go.disabled) return;
|
|
1057
1828
|
const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
if (
|
|
1829
|
+
if (!ws || !sd.pid) return;
|
|
1830
|
+
const bad = sd.extras.find((c) => commandProblem(c));
|
|
1831
|
+
if (sd.pid !== PLAIN && bad)
|
|
1832
|
+
return showStartError(new Error(t("pe.badCmd", { c: bad })));
|
|
1061
1833
|
go.disabled = true;
|
|
1062
1834
|
go.textContent = t("ag.starting");
|
|
1063
1835
|
try {
|
|
1836
|
+
if (sd.pid === PLAIN) {
|
|
1837
|
+
const r = await api(
|
|
1838
|
+
"POST",
|
|
1839
|
+
`/api/workspaces/${encodeURIComponent(ws)}/sessions`,
|
|
1840
|
+
);
|
|
1841
|
+
window.denShell?.toast?.(
|
|
1842
|
+
t("ag.new.started", { p: t("ag.session"), w: ws }),
|
|
1843
|
+
);
|
|
1844
|
+
dlg.close?.();
|
|
1845
|
+
await refresh();
|
|
1846
|
+
if (r?.session?.id) await openPane(r.session.id);
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1064
1849
|
const r = await api(
|
|
1065
1850
|
"POST",
|
|
1066
1851
|
`/api/workspaces/${encodeURIComponent(ws)}/agents`,
|
|
1067
|
-
|
|
1068
|
-
persona,
|
|
1069
|
-
...(runtime ? { runtime } : {}),
|
|
1070
|
-
},
|
|
1852
|
+
startBody(),
|
|
1071
1853
|
);
|
|
1072
|
-
window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ??
|
|
1854
|
+
window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ?? sd.pid }));
|
|
1855
|
+
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
1073
1856
|
dlg.close?.();
|
|
1074
1857
|
await refresh();
|
|
1075
1858
|
if (r?.agent?.sessionId) await openPane(r.agent.sessionId);
|
|
1076
1859
|
} catch (err) {
|
|
1077
|
-
|
|
1860
|
+
showStartError(err);
|
|
1078
1861
|
} finally {
|
|
1079
1862
|
go.disabled = false;
|
|
1080
1863
|
go.textContent = t("ag.go");
|
|
1864
|
+
if (dlg.open) paintRuntime(false);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/** New persona, Edit or Duplicate from the dialog; back to it after. */
|
|
1869
|
+
async function editFromStart(kind) {
|
|
1870
|
+
const p = curPersona();
|
|
1871
|
+
let src = null;
|
|
1872
|
+
if (kind !== "new" && p) {
|
|
1873
|
+
try {
|
|
1874
|
+
src = await personaDetail(p.id);
|
|
1875
|
+
} catch (err) {
|
|
1876
|
+
return void window.denShell?.errorToast?.(err);
|
|
1877
|
+
}
|
|
1081
1878
|
}
|
|
1879
|
+
openPersonaEditor({
|
|
1880
|
+
data: sd.data,
|
|
1881
|
+
src,
|
|
1882
|
+
dup: kind === "dup",
|
|
1883
|
+
onSaved: async (saved) => {
|
|
1884
|
+
personasAsked.clear();
|
|
1885
|
+
if (saved) sd.pid = saved.id;
|
|
1886
|
+
await fillForWorkspace($d("#ag-ws").value);
|
|
1887
|
+
},
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/** Wire the dialog once (mountAgentsPage). */
|
|
1892
|
+
function wireStartDialog(root) {
|
|
1893
|
+
const form = root.querySelector("#ag-form");
|
|
1894
|
+
form.addEventListener("click", (e) => {
|
|
1895
|
+
const pr = e.target.closest("#ag-persona [data-pid]");
|
|
1896
|
+
if (pr) {
|
|
1897
|
+
e.preventDefault();
|
|
1898
|
+
pickPersona(pr.dataset.pid);
|
|
1899
|
+
pr.focus();
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
const chip = e.target.closest("[data-pf]");
|
|
1903
|
+
if (chip) {
|
|
1904
|
+
e.preventDefault();
|
|
1905
|
+
sd.f[chip.dataset.pf] = chip.dataset.v;
|
|
1906
|
+
for (const c of form.querySelectorAll(`[data-pf="${chip.dataset.pf}"]`))
|
|
1907
|
+
c.setAttribute("aria-pressed", String(c.dataset.v === chip.dataset.v));
|
|
1908
|
+
return paintPlist(false);
|
|
1909
|
+
}
|
|
1910
|
+
const pm = e.target.closest("[data-pm]");
|
|
1911
|
+
if (pm) {
|
|
1912
|
+
e.preventDefault();
|
|
1913
|
+
if (pm.disabled) return;
|
|
1914
|
+
sd.intent = pm.dataset.pm;
|
|
1915
|
+
return syncPermBlock(form, "ag", sd.data, {
|
|
1916
|
+
intent: sd.intent,
|
|
1917
|
+
role: curPersona()?.role,
|
|
1918
|
+
extras: sd.extras,
|
|
1919
|
+
runtime: chosenRuntime(),
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
const x = e.target.closest("[data-x]")?.dataset.x;
|
|
1923
|
+
if (!x) return;
|
|
1924
|
+
e.preventDefault();
|
|
1925
|
+
if (x === "what") {
|
|
1926
|
+
sd.what = !sd.what;
|
|
1927
|
+
$d("#ag-what").hidden = !sd.what;
|
|
1928
|
+
e.target
|
|
1929
|
+
.closest("[data-x]")
|
|
1930
|
+
.setAttribute("aria-expanded", String(sd.what));
|
|
1931
|
+
} else if (x === "det") {
|
|
1932
|
+
sd.det = !sd.det;
|
|
1933
|
+
paintDetails();
|
|
1934
|
+
if (sd.det) void loadDetail();
|
|
1935
|
+
} else if (x === "clear") {
|
|
1936
|
+
sd.f = { q: "", cat: "", src: "" };
|
|
1937
|
+
paintPersonaField();
|
|
1938
|
+
} else if (x === "raw") window.denShell?.toast?.(t("pe.rawLater"));
|
|
1939
|
+
else if (x === "newp") void editFromStart("new");
|
|
1940
|
+
else if (x === "dup") void editFromStart("dup");
|
|
1941
|
+
else if (x === "edit") void editFromStart("edit");
|
|
1942
|
+
});
|
|
1943
|
+
form.addEventListener("input", (e) => {
|
|
1944
|
+
if (e.target.id === "ag-q") {
|
|
1945
|
+
sd.f.q = e.target.value;
|
|
1946
|
+
paintPlist(false);
|
|
1947
|
+
} else if (e.target.id === "ag-tools") {
|
|
1948
|
+
sd.extras = splitCommands(e.target.value);
|
|
1949
|
+
syncPermBlock(form, "ag", sd.data, {
|
|
1950
|
+
intent: sd.intent,
|
|
1951
|
+
role: curPersona()?.role,
|
|
1952
|
+
extras: sd.extras,
|
|
1953
|
+
runtime: chosenRuntime(),
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
form.addEventListener(
|
|
1958
|
+
"toggle",
|
|
1959
|
+
(e) => {
|
|
1960
|
+
if (e.target.id === "ag-xtra") sd.extrasOpen = e.target.open;
|
|
1961
|
+
},
|
|
1962
|
+
true,
|
|
1963
|
+
);
|
|
1964
|
+
form.addEventListener("change", (e) => {
|
|
1965
|
+
const id = e.target.id;
|
|
1966
|
+
if (id === "ag-ws") void fillForWorkspace(e.target.value);
|
|
1967
|
+
else if (id === "ag-runtime") {
|
|
1968
|
+
sd.model = "";
|
|
1969
|
+
sd.ver = "";
|
|
1970
|
+
$d("#ag-r-fb").hidden = true;
|
|
1971
|
+
paintRuntime(false);
|
|
1972
|
+
paintAdvanced();
|
|
1973
|
+
} else if (id === "ag-model") {
|
|
1974
|
+
sd.model = e.target.value;
|
|
1975
|
+
sd.ver = "";
|
|
1976
|
+
paintAdvanced();
|
|
1977
|
+
} else if (id === "ag-ver") sd.ver = e.target.value;
|
|
1978
|
+
});
|
|
1979
|
+
// ↑ ↓ move through the persona list (a radio group).
|
|
1980
|
+
root.querySelector("#ag-persona").addEventListener("keydown", (e) => {
|
|
1981
|
+
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
|
|
1982
|
+
const rows = [...e.currentTarget.querySelectorAll("[data-pid]")];
|
|
1983
|
+
const i = rows.indexOf(document.activeElement);
|
|
1984
|
+
const next = rows[i + (e.key === "ArrowDown" ? 1 : -1)];
|
|
1985
|
+
if (!next) return;
|
|
1986
|
+
e.preventDefault();
|
|
1987
|
+
pickPersona(next.dataset.pid);
|
|
1988
|
+
next.focus();
|
|
1989
|
+
});
|
|
1082
1990
|
}
|
|
1083
1991
|
|
|
1084
1992
|
/**
|
|
@@ -1125,14 +2033,33 @@ function translateStatic(root) {
|
|
|
1125
2033
|
set("#ag-h", "n.agents");
|
|
1126
2034
|
set("#ag-start", "ag.start");
|
|
1127
2035
|
set("#ag-clear", "ag.clear");
|
|
1128
|
-
set("#ag-dlg-h", "ag.
|
|
2036
|
+
set("#ag-dlg-h", "ag.new.title");
|
|
1129
2037
|
set("#ag-ws-t", "ag.ws");
|
|
1130
2038
|
set("#ag-p-t", "ag.persona");
|
|
1131
2039
|
set("#ag-r-t", "ag.runtime");
|
|
2040
|
+
set("#ag-what-b", "ag.new.what");
|
|
2041
|
+
set("#ag-newp", "ag.new.newP");
|
|
2042
|
+
set("#ag-what-p", "pe.what");
|
|
2043
|
+
set("#ag-task-t", "ag.new.task");
|
|
2044
|
+
set("#ag-adv-s", "ag.new.adv");
|
|
2045
|
+
set("#ag-note", "ag.new.note");
|
|
2046
|
+
const task = root.querySelector("#ag-task");
|
|
2047
|
+
if (task) task.placeholder = t("ag.new.taskPh");
|
|
2048
|
+
root
|
|
2049
|
+
.querySelector("#ag-persona")
|
|
2050
|
+
?.setAttribute("aria-label", t("ag.persona"));
|
|
1132
2051
|
set("#ag-cancel", "ag.cancel");
|
|
1133
2052
|
set("#ag-go", "ag.go");
|
|
1134
|
-
const
|
|
1135
|
-
|
|
2053
|
+
for (const b of root.querySelectorAll("[data-layouts] button")) {
|
|
2054
|
+
b.textContent = t(`ag.l.${b.dataset.layout}`);
|
|
2055
|
+
b.title = t("ag.l.tip", {
|
|
2056
|
+
name: b.textContent,
|
|
2057
|
+
key: `⌃⇧${LAYOUTS.indexOf(b.dataset.layout) + 1}`,
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
root
|
|
2061
|
+
.querySelector("[data-layouts]")
|
|
2062
|
+
?.setAttribute("aria-label", t("ag.layout"));
|
|
1136
2063
|
}
|
|
1137
2064
|
|
|
1138
2065
|
/** Fill the page drawn by agents.html and wire it. */
|
|
@@ -1143,6 +2070,7 @@ export async function mountAgentsPage(root) {
|
|
|
1143
2070
|
if (window.denShell?.apiOff?.()) return;
|
|
1144
2071
|
st.open = [];
|
|
1145
2072
|
const wanted = openFromUrl(location.search);
|
|
2073
|
+
const linked = layoutFromUrl(location.search);
|
|
1146
2074
|
const startAsked = new URLSearchParams(location.search).get("start") === "1";
|
|
1147
2075
|
|
|
1148
2076
|
root.querySelector("#agl").addEventListener("click", (e) => {
|
|
@@ -1166,14 +2094,29 @@ export async function mountAgentsPage(root) {
|
|
|
1166
2094
|
const b = e.target.closest(".agr");
|
|
1167
2095
|
if (!b) return;
|
|
1168
2096
|
if (b.getAttribute("aria-disabled") === "true") return;
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
2097
|
+
void pick(b.dataset.id, e.shiftKey || e.metaKey || e.ctrlKey);
|
|
2098
|
+
});
|
|
2099
|
+
root.querySelector("[data-layouts]")?.addEventListener("click", (e) => {
|
|
2100
|
+
const b = e.target.closest("button[data-layout]");
|
|
2101
|
+
if (b) setLayout(b.dataset.layout);
|
|
2102
|
+
});
|
|
2103
|
+
root.querySelector("#agr").addEventListener("dblclick", (e) => {
|
|
2104
|
+
const tab = e.target.closest('[role="tab"]');
|
|
2105
|
+
if (tab) renameTab(tab);
|
|
1172
2106
|
});
|
|
1173
2107
|
root.querySelector("#agr").addEventListener("click", (e) => {
|
|
2108
|
+
const tab = e.target.closest('[role="tab"]');
|
|
2109
|
+
if (tab) {
|
|
2110
|
+
if (st.cur === tab.dataset.id) return;
|
|
2111
|
+
st.cur = tab.dataset.id;
|
|
2112
|
+
paintLayout();
|
|
2113
|
+
paintList();
|
|
2114
|
+
tab.focus();
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
1174
2117
|
const b = e.target.closest("button[data-act]");
|
|
1175
2118
|
if (!b) return;
|
|
1176
|
-
const id = b.closest(".pane")?.dataset.id;
|
|
2119
|
+
const id = b.closest(".pane")?.dataset.id ?? b.dataset.id;
|
|
1177
2120
|
const p = id && st.panes.get(id);
|
|
1178
2121
|
if (!p) return;
|
|
1179
2122
|
if (b.dataset.act === "close") closePane(id);
|
|
@@ -1189,9 +2132,7 @@ export async function mountAgentsPage(root) {
|
|
|
1189
2132
|
root.querySelector("#ag-clear")?.addEventListener("click", (e) => {
|
|
1190
2133
|
void clearStopped(e.currentTarget);
|
|
1191
2134
|
});
|
|
1192
|
-
root
|
|
1193
|
-
void fillForWorkspace(e.currentTarget.value);
|
|
1194
|
-
});
|
|
2135
|
+
wireStartDialog(root);
|
|
1195
2136
|
root.querySelector("#ag-cancel").addEventListener("click", () => {
|
|
1196
2137
|
root.querySelector("#ag-dlg").close?.();
|
|
1197
2138
|
});
|
|
@@ -1203,7 +2144,7 @@ export async function mountAgentsPage(root) {
|
|
|
1203
2144
|
// cancels (the dialog's own cancel). A button keeps its own Enter.
|
|
1204
2145
|
root.querySelector("#ag-form").addEventListener("keydown", (e) => {
|
|
1205
2146
|
if (e.key !== "Enter" || e.isComposing) return;
|
|
1206
|
-
if (e.target.closest("button, textarea")) return;
|
|
2147
|
+
if (e.target.closest("button, textarea, summary")) return;
|
|
1207
2148
|
e.preventDefault();
|
|
1208
2149
|
if (!st.root.querySelector("#ag-go").disabled)
|
|
1209
2150
|
e.currentTarget.requestSubmit();
|
|
@@ -1217,6 +2158,33 @@ export async function mountAgentsPage(root) {
|
|
|
1217
2158
|
attributeFilter: ["data-mode", "data-theme", "class"],
|
|
1218
2159
|
});
|
|
1219
2160
|
if (!st.docWired) wireDocument();
|
|
2161
|
+
// ⌃⇧1 / ⌃⇧2 / ⌃⇧3 on every OS: the layouts, even from a terminal (whose
|
|
2162
|
+
// key handler never passes them to the agent). ⌘1-9 and Ctrl+1-9 are
|
|
2163
|
+
// the browser's (its tabs): the page leaves them alone.
|
|
2164
|
+
document.addEventListener(
|
|
2165
|
+
"keydown",
|
|
2166
|
+
(e) => {
|
|
2167
|
+
const i = layoutKeyIndex(e);
|
|
2168
|
+
if (i < 0 || st.root.querySelector("#ag-dlg")?.open) return;
|
|
2169
|
+
e.preventDefault();
|
|
2170
|
+
e.stopPropagation();
|
|
2171
|
+
setLayout(LAYOUTS[i]);
|
|
2172
|
+
},
|
|
2173
|
+
true,
|
|
2174
|
+
);
|
|
2175
|
+
// ⌘K: "Layout: Focus", "Layout: Tabs", "Layout: Tile".
|
|
2176
|
+
for (const l of LAYOUTS)
|
|
2177
|
+
window.denShell?.registerCommand?.({
|
|
2178
|
+
id: `agents-layout:${l}`,
|
|
2179
|
+
label: () => t("ag.pal.layout", { name: t(`ag.l.${l}`) }),
|
|
2180
|
+
keywords: ["layout", "agents", l, t(`ag.l.${l}`)],
|
|
2181
|
+
run: () => setLayout(l),
|
|
2182
|
+
});
|
|
2183
|
+
// The phone width comes and goes: Tabs there, the person's layout here.
|
|
2184
|
+
window.matchMedia?.(PHONE).addEventListener?.("change", () => {
|
|
2185
|
+
paintLayout();
|
|
2186
|
+
paintList();
|
|
2187
|
+
});
|
|
1220
2188
|
window.addEventListener("pagehide", () => {
|
|
1221
2189
|
for (const p of st.panes.values()) {
|
|
1222
2190
|
p.closed = true;
|
|
@@ -1228,7 +2196,9 @@ export async function mountAgentsPage(root) {
|
|
|
1228
2196
|
}
|
|
1229
2197
|
});
|
|
1230
2198
|
|
|
1231
|
-
await loadScope();
|
|
2199
|
+
const [, saved] = await Promise.all([loadScope(), savedLayout()]);
|
|
2200
|
+
// A link's &layout= wins for this page; else the person's.
|
|
2201
|
+
st.layout = linked ?? saved;
|
|
1232
2202
|
await refresh();
|
|
1233
2203
|
paintLayout();
|
|
1234
2204
|
// An id the list no longer has (removed, another distribution's) is
|