@monster-orange/den 0.4.12 → 0.4.14
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 +151 -0
- package/README.md +7 -7
- package/docs/distributions.md +189 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +227 -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 +128 -13
- 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 +33 -0
- package/src/shared/agents/roles.ts +288 -0
- package/src/shared/agents/runtime.ts +50 -0
- package/src/shared/den-config.ts +22 -1
- package/src/shared/environment-manager/service-runner.ts +31 -12
- package/src/shared/environment-manager/workspaces.ts +11 -0
- package/src/ui/cli/agent-ops.ts +796 -29
- package/src/ui/cli/agents-command.ts +97 -3
- package/src/ui/cli/env-probe.ts +4 -1
- package/src/ui/mcp/api.ts +12 -1
- package/src/ui/mcp/tools.ts +60 -1
- package/src/ui/server/agent-resume-grant.ts +35 -0
- package/src/ui/server/agent-routes.ts +412 -22
- package/src/ui/server/public/agent-start.js +3 -2
- package/src/ui/server/public/agents-page.js +693 -49
- package/src/ui/server/public/agents.html +76 -15
- package/src/ui/server/public/i18n/es.js +143 -0
- package/src/ui/server/public/i18n/ja.js +144 -0
- package/src/ui/server/public/persona-ui.css +563 -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 +22 -0
- package/src/ui/server/public/settings.html +1 -0
- package/src/ui/server/public/shell.js +140 -0
- package/src/ui/server/public/topbar.js +2 -2
- package/src/ui/server/request-guard.ts +17 -4
- package/src/ui/server/route-classes.ts +31 -0
- package/src/ui/server/session-routes.ts +21 -5
- package/src/ui/server/ui-assets.ts +3 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personas in the page (AG-10.4 to 10.7, AG-11, AG-12, AG-13): what the
|
|
3
|
+
* Start dialog (agents-page.js) and Settings › Personas share.
|
|
4
|
+
*
|
|
5
|
+
* - the filter bar (search, category chips, source chips) and the list,
|
|
6
|
+
* grouped by category when no filter is set (AG-10.4);
|
|
7
|
+
* - a persona's row: "Rita (reviewer)", its source badge, its one-line
|
|
8
|
+
* description, and "not installed" when its runtime isn't (AG-12.2);
|
|
9
|
+
* - Details: its instructions, runtime, model and version, permission
|
|
10
|
+
* mode, preset and extra commands (AG-10.6);
|
|
11
|
+
* - the permission block: the mode (AG-13.1), the role's preset as chips
|
|
12
|
+
* (13.3), extra commands (13.4) and the preview of what the runtime is
|
|
13
|
+
* given (13.2), from the runtime's own translation (GET /api/personas:
|
|
14
|
+
* runtimes[].permissions). A runtime that takes none from Den says
|
|
15
|
+
* "not supported on <runtime>" for each command, never drops one;
|
|
16
|
+
* - the persona editor (New persona, Edit, Duplicate: AG-10.7), a dialog
|
|
17
|
+
* of its own, and the Settings › Personas row.
|
|
18
|
+
*
|
|
19
|
+
* Every string a person or a pack wrote is escaped; every write goes to
|
|
20
|
+
* the persona routes (agent-routes.ts), which check every field again.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
|
|
24
|
+
export const esc = (s) =>
|
|
25
|
+
String(s ?? "").replace(
|
|
26
|
+
/[&<>"']/g,
|
|
27
|
+
(c) =>
|
|
28
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
29
|
+
c
|
|
30
|
+
],
|
|
31
|
+
);
|
|
32
|
+
const api = (method, url, body) => window.denShell.api(method, url, body);
|
|
33
|
+
|
|
34
|
+
/** Den's three permission intents (permissions.ts), in the switch's order. */
|
|
35
|
+
export const INTENTS = ["ask", "edits", "all"];
|
|
36
|
+
/** The sources a filter offers (hub: later, once Den Hub exists). */
|
|
37
|
+
export const ORIGINS = ["builtin", "distribution", "local"];
|
|
38
|
+
|
|
39
|
+
// The same rule as permissions.ts (the server checks again).
|
|
40
|
+
const PREFIX_RE = /^[A-Za-z0-9][A-Za-z0-9 ._/:=@%+,-]*$/;
|
|
41
|
+
const PATTERN_RE = /[*?[\]^$(){}|]/;
|
|
42
|
+
const MAX_COMMAND_LEN = 120;
|
|
43
|
+
|
|
44
|
+
/** Den's categories (roles.ts), for an answer that names none. */
|
|
45
|
+
const DEN_CATEGORIES = [
|
|
46
|
+
"engineering",
|
|
47
|
+
"design",
|
|
48
|
+
"product",
|
|
49
|
+
"marketing",
|
|
50
|
+
"operations",
|
|
51
|
+
].map((id) => ({ id, label: id, source: "den" }));
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* GET /api/personas' answer, filled in where an older or partial one
|
|
55
|
+
* leaves a field out: every persona has a category and a description,
|
|
56
|
+
* and there is a category list.
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeData(data) {
|
|
59
|
+
// A role that isn't a registry key is an older answer's description.
|
|
60
|
+
const key = (r) => typeof r === "string" && /^[a-z][a-z0-9-]{0,31}$/.test(r);
|
|
61
|
+
const personas = (data?.personas ?? []).map((p) => ({
|
|
62
|
+
...p,
|
|
63
|
+
role: key(p.role) ? p.role : undefined,
|
|
64
|
+
category: p.category || "engineering",
|
|
65
|
+
description: p.description ?? (key(p.role) ? "" : (p.role ?? "")),
|
|
66
|
+
allowedCommands: p.allowedCommands ?? [],
|
|
67
|
+
}));
|
|
68
|
+
return {
|
|
69
|
+
...data,
|
|
70
|
+
personas,
|
|
71
|
+
runtimes: data?.runtimes ?? [],
|
|
72
|
+
roles: data?.roles ?? [],
|
|
73
|
+
categories: data?.categories?.length ? data.categories : DEN_CATEGORIES,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Why `c` isn't a plain prefix ("pattern", "invalid", "long"), or null. */
|
|
78
|
+
export function commandProblem(c) {
|
|
79
|
+
if ([...c].length > MAX_COMMAND_LEN) return "long";
|
|
80
|
+
if (PATTERN_RE.test(c)) return "pattern";
|
|
81
|
+
if (!PREFIX_RE.test(c) || /\s{2,}/.test(c) || c !== c.trim())
|
|
82
|
+
return "invalid";
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One command per line, trimmed, blank lines dropped, each once. */
|
|
87
|
+
export function splitCommands(text) {
|
|
88
|
+
return [
|
|
89
|
+
...new Set(
|
|
90
|
+
String(text ?? "")
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map((x) => x.trim())
|
|
93
|
+
.filter(Boolean),
|
|
94
|
+
),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A category's label: Den's in the page's language, a distribution's as given. */
|
|
99
|
+
export function categoryLabel(categories, id) {
|
|
100
|
+
const c = (categories ?? []).find((x) => x.id === id);
|
|
101
|
+
if (!c) return id ?? "";
|
|
102
|
+
return c.source === "den" ? t(`cat.${c.id}`) : c.label;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A persona's name as HTML: "Rita <role>(reviewer)</role>", else its id (role). */
|
|
106
|
+
export function personaNameHtml(p) {
|
|
107
|
+
if (p.displayName)
|
|
108
|
+
return `${esc(p.displayName)} <span class="role">(${esc(p.role || p.id)})</span>`;
|
|
109
|
+
return `${esc(p.id)}${p.role && p.role !== p.id ? ` <span class="role">(${esc(p.role)})</span>` : ""}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A persona's name as text. */
|
|
113
|
+
export function personaName(p) {
|
|
114
|
+
if (!p) return "";
|
|
115
|
+
return p.displayName ? `${p.displayName} (${p.role || p.id})` : p.id;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const originOf = (p) => (ORIGINS.includes(p.origin) ? p.origin : "builtin");
|
|
119
|
+
|
|
120
|
+
/** Its source badge (AG-10.3): built-in, distribution, mine (hub later). */
|
|
121
|
+
export function originBadge(p) {
|
|
122
|
+
const o = originOf(p);
|
|
123
|
+
return `<span class="tagx src-${o}" title="${esc(t(`pe.src.${o}.p`))}">${esc(t(`pe.src.${o}`))}</span>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Whether persona `p` matches filter `f` ({ q, cat, src }). */
|
|
127
|
+
export function matchPersona(p, f, categories) {
|
|
128
|
+
if (f.cat && p.category !== f.cat) return false;
|
|
129
|
+
if (f.src && originOf(p) !== f.src) return false;
|
|
130
|
+
if (!f.q) return true;
|
|
131
|
+
const hay = [
|
|
132
|
+
p.displayName,
|
|
133
|
+
p.id,
|
|
134
|
+
p.role,
|
|
135
|
+
p.description,
|
|
136
|
+
categoryLabel(categories, p.category),
|
|
137
|
+
]
|
|
138
|
+
.join(" ")
|
|
139
|
+
.toLowerCase();
|
|
140
|
+
return f.q
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.split(/\s+/)
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.every((w) => hay.includes(w));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const noFilter = (f) => !f.q && !f.cat && !f.src;
|
|
148
|
+
|
|
149
|
+
/** The filter bar (AG-10.4): search, then the category and source chips. */
|
|
150
|
+
export function filterBarHtml(f, ctx, personas, categories) {
|
|
151
|
+
const chip = (k, v, label, n) =>
|
|
152
|
+
`<button type="button" class="pfc" data-pf="${k}" data-v="${esc(v)}" aria-pressed="${(f[k] || "") === v}">${esc(label)}${n != null ? `<span class="c">${n}</span>` : ""}</button>`;
|
|
153
|
+
// Only the categories some persona is in.
|
|
154
|
+
const cats = (categories ?? []).filter((c) =>
|
|
155
|
+
personas.some((p) => p.category === c.id),
|
|
156
|
+
);
|
|
157
|
+
return (
|
|
158
|
+
`<div class="pfbar"><input type="search" id="${ctx}-q" value="${esc(f.q)}" placeholder="${esc(t("pf.search"))}" aria-label="${esc(t("pf.search"))}" autocomplete="off" spellcheck="false">` +
|
|
159
|
+
`<div class="pfrow" role="group" aria-label="${esc(t("pf.cat"))}"><span class="lab">${esc(t("pf.cat"))}</span>${chip("cat", "", t("pf.all"))}` +
|
|
160
|
+
cats
|
|
161
|
+
.map((c) =>
|
|
162
|
+
chip(
|
|
163
|
+
"cat",
|
|
164
|
+
c.id,
|
|
165
|
+
categoryLabel(categories, c.id),
|
|
166
|
+
personas.filter((p) => p.category === c.id).length,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
.join("") +
|
|
170
|
+
`</div><div class="pfrow" role="group" aria-label="${esc(t("pf.src"))}"><span class="lab">${esc(t("pf.src"))}</span>${chip("src", "", t("pf.all"))}` +
|
|
171
|
+
ORIGINS.map((o) => chip("src", o, t(`pe.src.${o}`))).join("") +
|
|
172
|
+
`</div></div>`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The list: grouped under category headings with no filter set, else the
|
|
178
|
+
* matches alone; "No persona matches" when none does.
|
|
179
|
+
*/
|
|
180
|
+
export function personaListHtml(f, personas, categories, rowFn) {
|
|
181
|
+
const xs = personas.filter((p) => matchPersona(p, f, categories));
|
|
182
|
+
if (!xs.length)
|
|
183
|
+
return `<div class="pempty">${esc(t("pf.none"))} <button type="button" class="lnk" data-x="clear">${esc(t("pf.clear"))}</button></div>`;
|
|
184
|
+
if (!noFilter(f)) return xs.map(rowFn).join("");
|
|
185
|
+
const order = [
|
|
186
|
+
...(categories ?? []).map((c) => c.id),
|
|
187
|
+
...new Set(xs.map((p) => p.category)),
|
|
188
|
+
];
|
|
189
|
+
return [...new Set(order)]
|
|
190
|
+
.filter((id) => xs.some((p) => p.category === id))
|
|
191
|
+
.map((id) => {
|
|
192
|
+
const g = xs.filter((p) => p.category === id);
|
|
193
|
+
return `<div class="pgh"><span>${esc(categoryLabel(categories, id))}</span><span>${g.length}</span></div>${g.map(rowFn).join("")}`;
|
|
194
|
+
})
|
|
195
|
+
.join("");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const runtimeOf = (data, id) => (data?.runtimes ?? []).find((r) => r.id === id);
|
|
199
|
+
const runtimeLabel = (data, id) => runtimeOf(data, id)?.label || id || "";
|
|
200
|
+
|
|
201
|
+
/** The runtime a persona runs on (its own, else the default). */
|
|
202
|
+
export function personaRuntime(data, p) {
|
|
203
|
+
return p?.runtime || (data?.runtimes ?? []).find((r) => r.default)?.id || "";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Whether persona `p`'s own runtime is installed here. */
|
|
207
|
+
export function runtimeInstalled(data, p) {
|
|
208
|
+
return runtimeOf(data, personaRuntime(data, p))?.available !== false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A role's preset (AG-13.3), from the registry. */
|
|
212
|
+
export function rolePreset(data, role) {
|
|
213
|
+
return (data?.roles ?? []).find((r) => r.id === role)?.preset ?? [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** "latest" or a version, for people. */
|
|
217
|
+
export function versionLabel(v) {
|
|
218
|
+
return !v || v === "latest" ? t("ver.latest") : v;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The model a runtime offers by id, or undefined. */
|
|
222
|
+
export function modelOf(data, runtime, model) {
|
|
223
|
+
return (runtimeOf(data, runtime)?.models ?? []).find((m) => m.id === model);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Whether a pinned version is older than its model's newest (AG-12.4). */
|
|
227
|
+
export function isStale(data, runtime, model, version) {
|
|
228
|
+
const m = modelOf(data, runtime, model);
|
|
229
|
+
return Boolean(m && version && version !== "latest" && version !== m.latest);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const newerBadge = () =>
|
|
233
|
+
`<span class="tagx warn" data-newer title="${esc(t("ver.newerP"))}">${esc(t("ver.newer"))}</span>`;
|
|
234
|
+
|
|
235
|
+
/** A row of the Start list (a radio). */
|
|
236
|
+
export function personaRowHtml(p, checked, data) {
|
|
237
|
+
const missing = !runtimeInstalled(data, p)
|
|
238
|
+
? ` <span class="tagx warn">${esc(t("rt.notInst", { r: runtimeLabel(data, personaRuntime(data, p)) }))}</span>`
|
|
239
|
+
: "";
|
|
240
|
+
return (
|
|
241
|
+
`<button type="button" class="pr" role="radio" data-pid="${esc(p.id)}" aria-checked="${checked}">` +
|
|
242
|
+
`<span class="rd" aria-hidden="true"></span><span class="pn">${personaNameHtml(p)}${missing}</span>${originBadge(p)}` +
|
|
243
|
+
`<span class="pd">${esc(p.description || "")}</span></button>`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The permission preview (AG-13.2): what runtime `rt` is given. */
|
|
248
|
+
export function permPreviewHtml(data, rtId, intent, preset, extras) {
|
|
249
|
+
const rt = runtimeOf(data, rtId);
|
|
250
|
+
const name = runtimeLabel(data, rtId);
|
|
251
|
+
const perms = rt?.permissions;
|
|
252
|
+
const rule = (c) =>
|
|
253
|
+
perms ? perms.commandRules.map((r) => r.split("{c}").join(c)) : null;
|
|
254
|
+
const line = (c, src) => {
|
|
255
|
+
const why = commandProblem(c);
|
|
256
|
+
const rules = why ? null : rule(c);
|
|
257
|
+
const body = rules
|
|
258
|
+
? rules.map((r) => `<span class="mono">${esc(r)}</span>`).join(" ")
|
|
259
|
+
: `<span class="mono gone">${esc(c)}</span> <span class="tagx warn">${esc(
|
|
260
|
+
why === "pattern"
|
|
261
|
+
? t("pe.nsPat")
|
|
262
|
+
: why
|
|
263
|
+
? t("pe.nsBad")
|
|
264
|
+
: t("pe.ns", { r: name }),
|
|
265
|
+
)}</span>`;
|
|
266
|
+
return `<li data-cmd="${esc(c)}">${body} <span class="src">· ${esc(src)}</span></li>`;
|
|
267
|
+
};
|
|
268
|
+
const mode = perms
|
|
269
|
+
? `<li><span class="mono">${esc(perms.modes[intent] ?? "")}</span></li>`
|
|
270
|
+
: `<li><span class="tagx warn">${esc(t("pe.nsMode", { r: name }))}</span></li>`;
|
|
271
|
+
return (
|
|
272
|
+
`<div class="pv" data-runtime="${esc(rtId)}"><b>${esc(t("pe.allows", { r: name }))}</b><ul>` +
|
|
273
|
+
mode +
|
|
274
|
+
preset.map((c) => line(c, t("pe.preset").toLowerCase())).join("") +
|
|
275
|
+
extras.map((c) => line(c, t("pe.extraS"))).join("") +
|
|
276
|
+
`</ul></div>`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const presetChips = (preset) =>
|
|
281
|
+
preset.length
|
|
282
|
+
? preset.map((c) => `<span class="pchip">${esc(c)}</span>`).join("")
|
|
283
|
+
: `<span class="faint">—</span>`;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The permission block: mode, preset chips, extra commands (collapsed),
|
|
287
|
+
* the preview and the raw config stub (AG-13.5). `s` is
|
|
288
|
+
* { intent, role, extras, runtime, disabled }.
|
|
289
|
+
*/
|
|
290
|
+
export function permBlockHtml(ctx, data, s) {
|
|
291
|
+
const dis = s.disabled ? " disabled" : "";
|
|
292
|
+
const preset = rolePreset(data, s.role);
|
|
293
|
+
return (
|
|
294
|
+
`<div class="fld"><span>${esc(t("pe.perms"))}</span><div class="seg pmode" role="radiogroup" aria-label="${esc(t("pe.perms"))}">` +
|
|
295
|
+
INTENTS.map(
|
|
296
|
+
(m) =>
|
|
297
|
+
`<button type="button" data-pm="${m}" role="radio" aria-checked="${s.intent === m}" aria-pressed="${s.intent === m}"${dis}>${esc(t(`pm.${m}`))}</button>`,
|
|
298
|
+
).join("") +
|
|
299
|
+
`</div></div>` +
|
|
300
|
+
`<div class="fld"><span>${esc(t("pe.preset"))} · <span id="${ctx}-presetrole">${esc(s.role || "—")}</span></span><div class="pchips" id="${ctx}-preset">${presetChips(preset)}</div></div>` +
|
|
301
|
+
`<details class="xtra" id="${ctx}-xtra"${s.extrasOpen ? " open" : ""}><summary>${esc(t("pe.extra"))} (<span id="${ctx}-xn">${s.extras.length}</span>)</summary>` +
|
|
302
|
+
`<textarea id="${ctx}-tools" rows="3" spellcheck="false" aria-label="${esc(t("pe.extra"))}"${dis}>${esc(s.extras.join("\n"))}</textarea><small class="faint">${esc(t("pe.extraP"))}</small></details>` +
|
|
303
|
+
`<div class="pvw" id="${ctx}-pv">${permPreviewHtml(data, s.runtime, s.intent, preset, s.extras)}</div>` +
|
|
304
|
+
`<div class="helpl"><button type="button" class="lnk" data-x="raw" aria-disabled="true" title="${esc(t("pe.rawLater"))}">${esc(t("pe.raw"))}</button> <span class="tagx warn">${esc(t("pe.vendor"))}</span> <span class="tagx">${esc(t("rt.later"))}</span></div>`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Keep a permission block's preview, count and preset in step. */
|
|
309
|
+
export function syncPermBlock(root, ctx, data, s) {
|
|
310
|
+
const preset = rolePreset(data, s.role);
|
|
311
|
+
const pv = root.querySelector(`#${ctx}-pv`);
|
|
312
|
+
if (pv)
|
|
313
|
+
pv.innerHTML = permPreviewHtml(data, s.runtime, s.intent, preset, s.extras);
|
|
314
|
+
const xn = root.querySelector(`#${ctx}-xn`);
|
|
315
|
+
if (xn) xn.textContent = String(s.extras.length);
|
|
316
|
+
const pr = root.querySelector(`#${ctx}-preset`);
|
|
317
|
+
if (pr) pr.innerHTML = presetChips(preset);
|
|
318
|
+
const rl = root.querySelector(`#${ctx}-presetrole`);
|
|
319
|
+
if (rl) rl.textContent = s.role || "—";
|
|
320
|
+
for (const b of root.querySelectorAll("[data-pm]")) {
|
|
321
|
+
b.setAttribute("aria-pressed", String(b.dataset.pm === s.intent));
|
|
322
|
+
b.setAttribute("aria-checked", String(b.dataset.pm === s.intent));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Model options for runtime `rt`: "" first (labelled `first`), then its models. */
|
|
327
|
+
export function modelOptionsHtml(data, rt, cur, first) {
|
|
328
|
+
const ms = runtimeOf(data, rt)?.models ?? [];
|
|
329
|
+
return (
|
|
330
|
+
`<option value="">${esc(first)}</option>` +
|
|
331
|
+
ms
|
|
332
|
+
.map(
|
|
333
|
+
(m) =>
|
|
334
|
+
`<option value="${esc(m.id)}"${m.id === cur ? " selected" : ""}>${esc(m.label || m.id)}</option>`,
|
|
335
|
+
)
|
|
336
|
+
.join("")
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Version options for a model: "" (labelled `first`) when given, Latest, then each. */
|
|
341
|
+
export function versionOptionsHtml(data, rt, model, cur, first) {
|
|
342
|
+
const m = modelOf(data, rt, model);
|
|
343
|
+
const opts = [
|
|
344
|
+
...(first != null ? [["", first]] : []),
|
|
345
|
+
["latest", t("ver.latestP")],
|
|
346
|
+
...(m?.versions ?? []).map((v) => [v, v]),
|
|
347
|
+
];
|
|
348
|
+
return opts
|
|
349
|
+
.map(
|
|
350
|
+
([v, l]) =>
|
|
351
|
+
`<option value="${esc(v)}"${v === cur ? " selected" : ""}>${esc(l)}</option>`,
|
|
352
|
+
)
|
|
353
|
+
.join("");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Details (AG-10.6): `p` with its instructions (GET /api/personas/<id>). */
|
|
357
|
+
export function personaDetailHtml(p, data) {
|
|
358
|
+
const rt = personaRuntime(data, p);
|
|
359
|
+
const intent = p.permission || "ask";
|
|
360
|
+
const preset = rolePreset(data, p.role);
|
|
361
|
+
const extras = p.allowedCommands ?? [];
|
|
362
|
+
const list = (xs) =>
|
|
363
|
+
xs.length
|
|
364
|
+
? xs.map((x) => `<span class="mono">${esc(x)}</span>`).join(" · ")
|
|
365
|
+
: esc(t("pe.noneCmd"));
|
|
366
|
+
return (
|
|
367
|
+
`<div class="pdet" id="ag-det"><div class="why">${originBadge(p)} ${esc(t(`pe.src.${originOf(p)}.p`))}</div>` +
|
|
368
|
+
`<div><span class="k">${esc(t("pe.instr"))}</span><pre>${p.instructions == null ? esc(t("pe.loading")) : esc(p.instructions)}</pre></div>` +
|
|
369
|
+
`<div><span class="k">${esc(t("pe.runtime"))}</span> <span class="mono">${esc(runtimeLabel(data, rt))}</span> · <span class="k">${esc(t("pe.model"))}</span> <span class="mono">${esc(p.model || t("pe.rtDefault"))} · ${esc(versionLabel(p.version))}</span>${isStale(data, rt, p.model, p.version) ? ` ${newerBadge()}` : ""}</div>` +
|
|
370
|
+
`<div><span class="k">${esc(t("pe.perms"))}</span> ${esc(t(`pm.${intent}`))}</div>` +
|
|
371
|
+
`<div><span class="k">${esc(t("pe.preset"))}</span> ${list(preset)}</div>` +
|
|
372
|
+
`<div><span class="k">${esc(t("pe.extra"))}</span> ${list(extras)}</div>` +
|
|
373
|
+
`<div class="pvw">${permPreviewHtml(data, rt, intent, preset, extras)}</div></div>`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const detailAsked = new Map();
|
|
378
|
+
/** A persona with its instructions, asked once per page. */
|
|
379
|
+
export function personaDetail(id) {
|
|
380
|
+
if (!detailAsked.has(id))
|
|
381
|
+
detailAsked.set(
|
|
382
|
+
id,
|
|
383
|
+
api("GET", `/api/personas/${encodeURIComponent(id)}`)
|
|
384
|
+
.then((r) => r.persona)
|
|
385
|
+
.catch((e) => {
|
|
386
|
+
detailAsked.delete(id);
|
|
387
|
+
throw e;
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
return detailAsked.get(id);
|
|
391
|
+
}
|
|
392
|
+
/** Forget what was asked (after a save or delete). */
|
|
393
|
+
export function forgetPersonaDetails() {
|
|
394
|
+
detailAsked.clear();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The tags a session's row carries (AG-10.9, AG-12.4, AG-13.1): what it
|
|
399
|
+
* overrides of its persona, Auto-approve all always, and "newer available"
|
|
400
|
+
* when it is pinned to an older version. HTML.
|
|
401
|
+
*/
|
|
402
|
+
export function overrideTagsHtml(r) {
|
|
403
|
+
const ov = r.overrides ?? [];
|
|
404
|
+
const tags = [];
|
|
405
|
+
if (ov.includes("runtime") && r.runtime)
|
|
406
|
+
tags.push(["", t("ov.runtime", { r: r.runtime }), "runtime"]);
|
|
407
|
+
if (ov.includes("model") && r.model)
|
|
408
|
+
tags.push(["", t("ov.model", { m: r.model }), "model"]);
|
|
409
|
+
if (ov.includes("version") && r.version)
|
|
410
|
+
tags.push([
|
|
411
|
+
"",
|
|
412
|
+
r.version === "latest"
|
|
413
|
+
? t("ov.latest")
|
|
414
|
+
: t("ver.pinned", { v: r.version }),
|
|
415
|
+
"version",
|
|
416
|
+
]);
|
|
417
|
+
if (r.permission === "all")
|
|
418
|
+
tags.push(["warn", t("pm.tag.all"), "permission"]);
|
|
419
|
+
else if (ov.includes("permission") && r.permission)
|
|
420
|
+
tags.push([
|
|
421
|
+
r.permission === "ask" ? "" : "warn",
|
|
422
|
+
t(`pm.tag.${r.permission}`),
|
|
423
|
+
"permission",
|
|
424
|
+
]);
|
|
425
|
+
if (ov.includes("allow")) tags.push(["", t("ov.tools"), "allow"]);
|
|
426
|
+
if (r.newer) tags.push(["warn", t("ver.newer"), "newer"]);
|
|
427
|
+
return tags
|
|
428
|
+
.map(
|
|
429
|
+
([cls, label, k]) =>
|
|
430
|
+
`<span class="tagx${cls ? ` ${cls}` : ""}" data-ov="${k}"${k === "newer" ? ` title="${esc(t("ver.newerOf", { v: r.newer }))}"` : ""}>${esc(label)}</span>`,
|
|
431
|
+
)
|
|
432
|
+
.join("");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── The persona editor (AG-10.7) ─────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
/** The fields the editor sends (persona-store.ts KEYS). */
|
|
438
|
+
function editorBody(dlg, s) {
|
|
439
|
+
const v = (sel) => dlg.querySelector(sel)?.value ?? "";
|
|
440
|
+
return {
|
|
441
|
+
displayName: v("#pe-name").trim(),
|
|
442
|
+
role: v("#pe-role"),
|
|
443
|
+
category: v("#pe-cat"),
|
|
444
|
+
description: v("#pe-desc").trim(),
|
|
445
|
+
instructions: v("#pe-instr"),
|
|
446
|
+
runtime: v("#pe-rt"),
|
|
447
|
+
model: v("#pe-model"),
|
|
448
|
+
version: v("#pe-ver") || "latest",
|
|
449
|
+
permission: s.intent,
|
|
450
|
+
allowedCommands: splitCommands(v("#pe-tools")),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function runtimeOptionsHtml(data, cur) {
|
|
455
|
+
const rts = (data?.runtimes ?? []).filter((r) => r.id !== "generic");
|
|
456
|
+
const opt = (r) =>
|
|
457
|
+
`<option value="${esc(r.id)}"${r.id === cur ? " selected" : ""}>${esc(r.label || r.id)}${r.available ? (r.version ? ` · ${esc(r.version)}` : "") : ` · ${esc(t("rt.missing"))}`}</option>`;
|
|
458
|
+
const ins = rts.filter((r) => r.available);
|
|
459
|
+
const out = rts.filter((r) => !r.available);
|
|
460
|
+
return (
|
|
461
|
+
`<optgroup label="${esc(t("rt.installed"))}">${ins.map(opt).join("")}</optgroup>` +
|
|
462
|
+
(out.length
|
|
463
|
+
? `<optgroup label="${esc(t("rt.missing"))}">${out.map(opt).join("")}</optgroup>`
|
|
464
|
+
: "")
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Open the persona editor. `o`: { data (GET /api/personas), src (a
|
|
470
|
+
* persona with instructions, to edit or duplicate; none: New), dup,
|
|
471
|
+
* view (read-only), onSaved(persona), onClosed() }. A persona that isn't
|
|
472
|
+
* local opens as View, with Duplicate.
|
|
473
|
+
*/
|
|
474
|
+
export function openPersonaEditor(o) {
|
|
475
|
+
const data = o.data;
|
|
476
|
+
const src = o.src ?? null;
|
|
477
|
+
const ro = Boolean(src && !o.dup && src.origin !== "local");
|
|
478
|
+
const isNew = !src || o.dup;
|
|
479
|
+
const roles = data?.roles ?? [];
|
|
480
|
+
const firstRole = roles.find((r) => r.id === "sde") ?? roles[0];
|
|
481
|
+
const d = src
|
|
482
|
+
? {
|
|
483
|
+
displayName: o.dup
|
|
484
|
+
? t("pe.copy", { n: src.displayName || src.id })
|
|
485
|
+
: src.displayName || "",
|
|
486
|
+
role: src.role || firstRole?.id || "",
|
|
487
|
+
category: src.category || "engineering",
|
|
488
|
+
description: src.description || "",
|
|
489
|
+
instructions: src.instructions || "",
|
|
490
|
+
runtime: src.runtime || personaRuntime(data, src),
|
|
491
|
+
model: src.model || "",
|
|
492
|
+
version: src.version || "latest",
|
|
493
|
+
permission: src.permission || "ask",
|
|
494
|
+
allowedCommands: [...(src.allowedCommands ?? [])],
|
|
495
|
+
}
|
|
496
|
+
: {
|
|
497
|
+
displayName: "",
|
|
498
|
+
role: firstRole?.id || "",
|
|
499
|
+
category: firstRole?.category || "engineering",
|
|
500
|
+
description: "",
|
|
501
|
+
instructions: "",
|
|
502
|
+
runtime: personaRuntime(data, null),
|
|
503
|
+
model: "",
|
|
504
|
+
version: "latest",
|
|
505
|
+
permission: firstRole?.mode || "ask",
|
|
506
|
+
allowedCommands: [],
|
|
507
|
+
};
|
|
508
|
+
const s = {
|
|
509
|
+
intent: d.permission,
|
|
510
|
+
role: d.role,
|
|
511
|
+
runtime: d.runtime,
|
|
512
|
+
extras: d.allowedCommands,
|
|
513
|
+
};
|
|
514
|
+
const dis = ro ? " disabled" : "";
|
|
515
|
+
const title = ro
|
|
516
|
+
? personaName(src)
|
|
517
|
+
: isNew
|
|
518
|
+
? t(o.dup ? "pe.dupT" : "pe.new")
|
|
519
|
+
: t("pe.edit");
|
|
520
|
+
const dlg = document.createElement("dialog");
|
|
521
|
+
dlg.className = "ag-dlg pe-dlg";
|
|
522
|
+
dlg.id = "pe-dlg";
|
|
523
|
+
dlg.setAttribute("aria-labelledby", "pe-h");
|
|
524
|
+
const cats = data?.categories ?? [];
|
|
525
|
+
dlg.innerHTML =
|
|
526
|
+
`<form method="dialog" id="pe-form" novalidate>` +
|
|
527
|
+
`<h2 id="pe-h">${esc(title)}</h2>` +
|
|
528
|
+
(ro ? `<p class="help">${originBadge(src)} ${esc(t("pe.ro"))}</p>` : "") +
|
|
529
|
+
`<div class="two"><label class="fld"><span>${esc(t("pe.name"))}</span><input type="text" id="pe-name" maxlength="40" value="${esc(d.displayName)}" placeholder="${esc(t("pe.nameP"))}"${dis}></label>` +
|
|
530
|
+
`<label class="fld"><span>${esc(t("pe.role"))}</span><select id="pe-role"${dis}>${roles.map((r) => `<option value="${esc(r.id)}"${r.id === d.role ? " selected" : ""}>${esc(r.id)}</option>`).join("")}</select></label></div>` +
|
|
531
|
+
`<div class="helpl">${esc(t("pe.roleHelp"))}</div>` +
|
|
532
|
+
`<div class="two"><label class="fld"><span>${esc(t("pe.cat"))}</span><select id="pe-cat"${dis}>${cats.map((c) => `<option value="${esc(c.id)}"${c.id === d.category ? " selected" : ""}>${esc(categoryLabel(cats, c.id))}</option>`).join("")}</select></label>` +
|
|
533
|
+
`<label class="fld"><span>${esc(t("pe.desc"))}</span><input type="text" id="pe-desc" maxlength="200" value="${esc(d.description)}"${dis}></label></div>` +
|
|
534
|
+
`<label class="fld"><span>${esc(t("pe.instr"))}</span><textarea id="pe-instr" rows="5" spellcheck="true"${dis}>${esc(d.instructions)}</textarea></label>` +
|
|
535
|
+
`<div class="two"><label class="fld"><span>${esc(t("pe.runtime"))}</span><select id="pe-rt"${dis}>${runtimeOptionsHtml(data, d.runtime)}</select></label>` +
|
|
536
|
+
`<label class="fld"><span>${esc(t("pe.model"))}</span><select id="pe-model"${dis}>${modelOptionsHtml(data, d.runtime, d.model, t("pe.rtDefault"))}</select></label></div>` +
|
|
537
|
+
`<div class="two"><label class="fld"><span>${esc(t("pe.ver"))}</span><select id="pe-ver"${dis}>${versionOptionsHtml(data, d.runtime, d.model, d.version)}</select></label><span class="helpl" id="pe-newer">${isStale(data, d.runtime, d.model, d.version) ? `${newerBadge()} <button type="button" class="lnk" data-x="latest">${esc(t("ver.use"))}</button>` : ""}</span></div>` +
|
|
538
|
+
`<div class="pblk">${permBlockHtml("pe", data, { ...s, disabled: ro, extrasOpen: s.extras.length > 0 })}</div>` +
|
|
539
|
+
`<p class="err" id="pe-err" role="alert" hidden></p>` +
|
|
540
|
+
`<div class="btns">${!isNew && src.origin === "local" ? `<button type="button" class="den-btn" data-x="del">${esc(t("pe.del"))}</button>` : ""}<span class="sp"></span>` +
|
|
541
|
+
`<button type="button" class="den-btn" data-x="cancel">${esc(t("ag.cancel"))}</button>` +
|
|
542
|
+
(ro
|
|
543
|
+
? `<button type="button" class="den-btn primary" data-x="dup">${esc(t("pe.dup"))}</button>`
|
|
544
|
+
: `<button type="submit" class="den-btn primary" id="pe-go">${esc(t("pe.save"))}</button>`) +
|
|
545
|
+
`</div></form>`;
|
|
546
|
+
document.body.append(dlg);
|
|
547
|
+
const $ = (sel) => dlg.querySelector(sel);
|
|
548
|
+
const close = () => {
|
|
549
|
+
dlg.close?.();
|
|
550
|
+
dlg.remove();
|
|
551
|
+
o.onClosed?.();
|
|
552
|
+
};
|
|
553
|
+
dlg.addEventListener("cancel", (e) => {
|
|
554
|
+
e.preventDefault();
|
|
555
|
+
close();
|
|
556
|
+
});
|
|
557
|
+
const sync = () => {
|
|
558
|
+
s.extras = splitCommands($("#pe-tools").value);
|
|
559
|
+
s.role = $("#pe-role").value;
|
|
560
|
+
s.runtime = $("#pe-rt").value;
|
|
561
|
+
syncPermBlock(dlg, "pe", data, s);
|
|
562
|
+
};
|
|
563
|
+
// A role fills a new persona in from its template, never over what is
|
|
564
|
+
// already typed (AG-11.2).
|
|
565
|
+
const prev = { instr: d.instructions, intent: s.intent, cat: d.category };
|
|
566
|
+
const prefill = () => {
|
|
567
|
+
if (!isNew || o.dup) return;
|
|
568
|
+
const r = roles.find((x) => x.id === $("#pe-role").value);
|
|
569
|
+
if (!r) return;
|
|
570
|
+
const ti = $("#pe-instr");
|
|
571
|
+
if (ti.value === prev.instr) ti.value = prev.instr = r.instructions || "";
|
|
572
|
+
if (s.intent === prev.intent) s.intent = prev.intent = r.mode || "ask";
|
|
573
|
+
const cs = $("#pe-cat");
|
|
574
|
+
if (cs.value === prev.cat && r.category) cs.value = prev.cat = r.category;
|
|
575
|
+
};
|
|
576
|
+
if (isNew && !o.dup) prefill();
|
|
577
|
+
sync();
|
|
578
|
+
$("#pe-role").addEventListener("change", () => {
|
|
579
|
+
prefill();
|
|
580
|
+
sync();
|
|
581
|
+
});
|
|
582
|
+
$("#pe-tools").addEventListener("input", sync);
|
|
583
|
+
const newer = () => {
|
|
584
|
+
$("#pe-newer").innerHTML = isStale(
|
|
585
|
+
data,
|
|
586
|
+
$("#pe-rt").value,
|
|
587
|
+
$("#pe-model").value,
|
|
588
|
+
$("#pe-ver").value,
|
|
589
|
+
)
|
|
590
|
+
? `${newerBadge()} <button type="button" class="lnk" data-x="latest">${esc(t("ver.use"))}</button>`
|
|
591
|
+
: "";
|
|
592
|
+
};
|
|
593
|
+
$("#pe-rt").addEventListener("change", () => {
|
|
594
|
+
$("#pe-model").innerHTML = modelOptionsHtml(
|
|
595
|
+
data,
|
|
596
|
+
$("#pe-rt").value,
|
|
597
|
+
"",
|
|
598
|
+
t("pe.rtDefault"),
|
|
599
|
+
);
|
|
600
|
+
$("#pe-ver").innerHTML = versionOptionsHtml(
|
|
601
|
+
data,
|
|
602
|
+
$("#pe-rt").value,
|
|
603
|
+
"",
|
|
604
|
+
"latest",
|
|
605
|
+
);
|
|
606
|
+
sync();
|
|
607
|
+
newer();
|
|
608
|
+
});
|
|
609
|
+
$("#pe-model").addEventListener("change", () => {
|
|
610
|
+
$("#pe-ver").innerHTML = versionOptionsHtml(
|
|
611
|
+
data,
|
|
612
|
+
$("#pe-rt").value,
|
|
613
|
+
$("#pe-model").value,
|
|
614
|
+
"latest",
|
|
615
|
+
);
|
|
616
|
+
newer();
|
|
617
|
+
});
|
|
618
|
+
$("#pe-ver").addEventListener("change", newer);
|
|
619
|
+
dlg.addEventListener("click", async (e) => {
|
|
620
|
+
const pm = e.target.closest("[data-pm]");
|
|
621
|
+
if (pm && !pm.disabled) {
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
s.intent = pm.dataset.pm;
|
|
624
|
+
sync();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const b = e.target.closest("[data-x]");
|
|
628
|
+
if (!b) return;
|
|
629
|
+
const x = b.dataset.x;
|
|
630
|
+
if (x === "cancel") close();
|
|
631
|
+
else if (x === "raw") window.denShell?.toast?.(t("pe.rawLater"));
|
|
632
|
+
else if (x === "latest") {
|
|
633
|
+
$("#pe-ver").value = "latest";
|
|
634
|
+
newer();
|
|
635
|
+
} else if (x === "dup") {
|
|
636
|
+
close();
|
|
637
|
+
openPersonaEditor({ ...o, dup: true });
|
|
638
|
+
} else if (x === "del") {
|
|
639
|
+
try {
|
|
640
|
+
await api("DELETE", `/api/personas/${encodeURIComponent(src.id)}`);
|
|
641
|
+
forgetPersonaDetails();
|
|
642
|
+
window.denShell?.toast?.(t("pe.deleted", { n: personaName(src) }));
|
|
643
|
+
dlg.close?.();
|
|
644
|
+
dlg.remove();
|
|
645
|
+
await o.onSaved?.(null);
|
|
646
|
+
} catch (err) {
|
|
647
|
+
showErr(err);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
const showErr = (err) => {
|
|
652
|
+
const el = $("#pe-err");
|
|
653
|
+
el.textContent = err?.message || String(err);
|
|
654
|
+
el.hidden = false;
|
|
655
|
+
};
|
|
656
|
+
$("#pe-form").addEventListener("submit", async (e) => {
|
|
657
|
+
e.preventDefault();
|
|
658
|
+
if (ro) return;
|
|
659
|
+
const body = editorBody(dlg, s);
|
|
660
|
+
const bad = body.allowedCommands.find((c) => commandProblem(c));
|
|
661
|
+
if (bad) return showErr(new Error(t("pe.badCmd", { c: bad })));
|
|
662
|
+
const go = $("#pe-go");
|
|
663
|
+
go.disabled = true;
|
|
664
|
+
try {
|
|
665
|
+
const r = isNew
|
|
666
|
+
? await api("POST", "/api/personas", body)
|
|
667
|
+
: await api(
|
|
668
|
+
"POST",
|
|
669
|
+
`/api/personas/${encodeURIComponent(src.id)}`,
|
|
670
|
+
body,
|
|
671
|
+
);
|
|
672
|
+
forgetPersonaDetails();
|
|
673
|
+
window.denShell?.toast?.(t("pe.saved", { n: personaName(r.persona) }));
|
|
674
|
+
dlg.close?.();
|
|
675
|
+
dlg.remove();
|
|
676
|
+
await o.onSaved?.(r.persona);
|
|
677
|
+
} catch (err) {
|
|
678
|
+
showErr(err);
|
|
679
|
+
} finally {
|
|
680
|
+
go.disabled = false;
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
if (typeof dlg.showModal === "function") dlg.showModal();
|
|
684
|
+
else dlg.setAttribute("open", "");
|
|
685
|
+
(ro ? $('[data-x="dup"]') : $("#pe-name"))?.focus();
|
|
686
|
+
return dlg;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ── Settings › Personas (AG-10.7) ────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* The Settings › Personas row: the filter bar, the list (View or Edit,
|
|
693
|
+
* Duplicate, Delete for your own), "+ New persona", and "newer
|
|
694
|
+
* available" on one pinned to an older version (AG-12.4).
|
|
695
|
+
*/
|
|
696
|
+
export function renderPersonasSettings(host, ctx) {
|
|
697
|
+
const f = { q: "", cat: "", src: "" };
|
|
698
|
+
let data = null;
|
|
699
|
+
const row = (p) => {
|
|
700
|
+
const mine = p.origin === "local";
|
|
701
|
+
const rt = personaRuntime(data, p);
|
|
702
|
+
const stale = isStale(data, rt, p.model, p.version);
|
|
703
|
+
return (
|
|
704
|
+
`<div class="prow" data-pid="${esc(p.id)}"><div class="pn">${personaNameHtml(p)} ${originBadge(p)}${stale ? ` ${newerBadge()}${mine ? ` <button type="button" class="lnk" data-x="latest">${esc(t("ver.use"))}</button>` : ""}` : ""}</div>` +
|
|
705
|
+
`<div class="pd">${esc(p.description || "")}</div>` +
|
|
706
|
+
`<div class="pm"><span class="mono">${esc(runtimeLabel(data, rt))}${p.model ? ` · ${esc(p.model)} · ${esc(versionLabel(p.version))}` : ""}</span> · ${esc(t(`pm.${p.permission || "ask"}`))}</div>` +
|
|
707
|
+
`<div class="pa"><button type="button" class="den-btn sm" data-x="${mine ? "edit" : "view"}">${esc(t(mine ? "pe.editB" : "pe.view"))}</button>` +
|
|
708
|
+
`<button type="button" class="den-btn sm" data-x="dup">${esc(t("pe.dup"))}</button>` +
|
|
709
|
+
(mine
|
|
710
|
+
? `<button type="button" class="den-btn sm" data-x="del">${esc(t("pe.del"))}</button>`
|
|
711
|
+
: "") +
|
|
712
|
+
`</div></div>`
|
|
713
|
+
);
|
|
714
|
+
};
|
|
715
|
+
const drawList = () => {
|
|
716
|
+
const l = host.querySelector(".plist");
|
|
717
|
+
if (l)
|
|
718
|
+
l.innerHTML = personaListHtml(f, data.personas, data.categories, row);
|
|
719
|
+
};
|
|
720
|
+
const draw = () => {
|
|
721
|
+
host.innerHTML =
|
|
722
|
+
`<div class="pe-set"><div class="fldh"><span class="helpl">${esc(t("pe.what"))}</span><span class="sp"></span><button type="button" class="den-btn sm" data-x="new">${esc(t("ag.new.newP"))}</button></div>` +
|
|
723
|
+
filterBarHtml(f, "set", data.personas, data.categories) +
|
|
724
|
+
`<div class="plist" role="list" aria-label="${esc(t("set.agents.personas"))}"></div></div>`;
|
|
725
|
+
drawList();
|
|
726
|
+
};
|
|
727
|
+
const load = async () => {
|
|
728
|
+
// Under an invalid config the personas answer 422: say why, ask nothing.
|
|
729
|
+
if ((ctx?.shell ?? window.denShell)?.configInvalid?.() === true) {
|
|
730
|
+
host.innerHTML = `<div class="set-note">${esc(t("pe.configFirst"))}</div>`;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
data = normalizeData(await api("GET", "/api/personas"));
|
|
735
|
+
draw();
|
|
736
|
+
} catch (e) {
|
|
737
|
+
host.innerHTML = `<div class="set-note">${esc(e?.message || t("pe.loadFail"))}</div>`;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
host.addEventListener("input", (e) => {
|
|
741
|
+
if (e.target.id !== "set-q") return;
|
|
742
|
+
f.q = e.target.value;
|
|
743
|
+
drawList();
|
|
744
|
+
});
|
|
745
|
+
host.addEventListener("click", async (e) => {
|
|
746
|
+
const chip = e.target.closest("[data-pf]");
|
|
747
|
+
if (chip) {
|
|
748
|
+
f[chip.dataset.pf] = chip.dataset.v;
|
|
749
|
+
for (const c of host.querySelectorAll(`[data-pf="${chip.dataset.pf}"]`))
|
|
750
|
+
c.setAttribute("aria-pressed", String(c.dataset.v === chip.dataset.v));
|
|
751
|
+
return drawList();
|
|
752
|
+
}
|
|
753
|
+
const b = e.target.closest("[data-x]");
|
|
754
|
+
if (!b) return;
|
|
755
|
+
const x = b.dataset.x;
|
|
756
|
+
const reload = async () => {
|
|
757
|
+
forgetPersonaDetails();
|
|
758
|
+
await load();
|
|
759
|
+
};
|
|
760
|
+
if (x === "clear") {
|
|
761
|
+
f.q = f.cat = f.src = "";
|
|
762
|
+
return draw();
|
|
763
|
+
}
|
|
764
|
+
if (x === "new") return void openPersonaEditor({ data, onSaved: reload });
|
|
765
|
+
const id = b.closest("[data-pid]")?.dataset.pid;
|
|
766
|
+
if (!id) return;
|
|
767
|
+
try {
|
|
768
|
+
const p = await personaDetail(id);
|
|
769
|
+
if (x === "del") {
|
|
770
|
+
await api("DELETE", `/api/personas/${encodeURIComponent(id)}`);
|
|
771
|
+
window.denShell?.toast?.(t("pe.deleted", { n: personaName(p) }));
|
|
772
|
+
return void reload();
|
|
773
|
+
}
|
|
774
|
+
if (x === "latest") {
|
|
775
|
+
await api("POST", `/api/personas/${encodeURIComponent(id)}`, {
|
|
776
|
+
displayName: p.displayName || "",
|
|
777
|
+
role: p.role,
|
|
778
|
+
category: p.category,
|
|
779
|
+
description: p.description,
|
|
780
|
+
instructions: p.instructions,
|
|
781
|
+
runtime: personaRuntime(data, p),
|
|
782
|
+
model: p.model || "",
|
|
783
|
+
version: "latest",
|
|
784
|
+
permission: p.permission || "ask",
|
|
785
|
+
allowedCommands: p.allowedCommands ?? [],
|
|
786
|
+
});
|
|
787
|
+
window.denShell?.toast?.(t("ver.usedLatest", { n: personaName(p) }));
|
|
788
|
+
return void reload();
|
|
789
|
+
}
|
|
790
|
+
openPersonaEditor({ data, src: p, dup: x === "dup", onSaved: reload });
|
|
791
|
+
} catch (err) {
|
|
792
|
+
window.denShell?.errorToast?.(err);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
void load();
|
|
796
|
+
}
|