@monster-orange/den 0.4.13 → 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 +49 -0
- package/README.md +7 -7
- package/docs/distributions.md +43 -1
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +227 -0
- package/src/shared/agents/den-claude.ts +9 -0
- package/src/shared/agents/records.ts +5 -0
- package/src/shared/agents/runtime.ts +9 -0
- package/src/shared/den-config.ts +2 -0
- 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 +266 -17
- package/src/ui/cli/agents-command.ts +85 -1
- package/src/ui/mcp/api.ts +12 -1
- package/src/ui/mcp/tools.ts +59 -0
- package/src/ui/server/agent-resume-grant.ts +35 -0
- package/src/ui/server/agent-routes.ts +152 -1
- package/src/ui/server/public/agents-page.js +201 -8
- package/src/ui/server/public/agents.html +18 -0
- package/src/ui/server/public/i18n/es.js +23 -0
- package/src/ui/server/public/i18n/ja.js +24 -0
- package/src/ui/server/public/persona-ui.css +24 -0
- package/src/ui/server/public/shell.js +23 -0
- package/src/ui/server/request-guard.ts +17 -4
- package/src/ui/server/route-classes.ts +13 -0
|
@@ -29,6 +29,18 @@
|
|
|
29
29
|
* instruction is typed into
|
|
30
30
|
* the session once it is
|
|
31
31
|
* ready, never an argument
|
|
32
|
+
* GET /api/workspaces/<ws>/conversations[?runtime=<r>] Bring in
|
|
33
|
+
* (AG-17.1): each runtime's
|
|
34
|
+
* own conversations in the
|
|
35
|
+
* workspace's folder, newest
|
|
36
|
+
* first, with which runtimes
|
|
37
|
+
* can bring one in
|
|
38
|
+
* POST /api/workspaces/<ws>/conversations/<id>/import { persona,
|
|
39
|
+
* runtime?, id? }: the
|
|
40
|
+
* runtime's resume of it, as
|
|
41
|
+
* a Den agent; 409
|
|
42
|
+
* open_elsewhere when it was
|
|
43
|
+
* written in the last minute
|
|
32
44
|
* POST /api/workspaces/<ws>/sessions a plain session: the login
|
|
33
45
|
* shell in the workspace's
|
|
34
46
|
* folder (AG-10.1)
|
|
@@ -86,12 +98,14 @@ import {
|
|
|
86
98
|
} from "../../shared/den-config.js";
|
|
87
99
|
import {
|
|
88
100
|
readWorkspacesFile,
|
|
101
|
+
repoFolders,
|
|
89
102
|
resolveWorkspaceDir,
|
|
90
103
|
} from "../../shared/environment-manager/workspaces.js";
|
|
91
104
|
import { getStateDir } from "../../shared/paths.js";
|
|
92
105
|
import { HostError } from "../../shared/sessions/client.js";
|
|
93
106
|
import {
|
|
94
107
|
AgentError,
|
|
108
|
+
defaultId,
|
|
95
109
|
agentRecord,
|
|
96
110
|
checkInstruction,
|
|
97
111
|
createPersona,
|
|
@@ -114,6 +128,8 @@ import {
|
|
|
114
128
|
resumeInterrupted,
|
|
115
129
|
setRuntimePath,
|
|
116
130
|
resolveAgentsDeps,
|
|
131
|
+
importAgent,
|
|
132
|
+
listConversations,
|
|
117
133
|
relaunchAgent,
|
|
118
134
|
resumeAgent,
|
|
119
135
|
startAgent,
|
|
@@ -131,7 +147,10 @@ import {
|
|
|
131
147
|
auditActorOf,
|
|
132
148
|
principalOf,
|
|
133
149
|
} from "./request-guard.js";
|
|
134
|
-
import {
|
|
150
|
+
import {
|
|
151
|
+
agentImportRefusal,
|
|
152
|
+
agentResumeRefusal,
|
|
153
|
+
} from "./agent-resume-grant.js";
|
|
135
154
|
import { readBody, sendJson } from "./http-helpers.js";
|
|
136
155
|
import type { Actor } from "../../shared/agent-actor.js";
|
|
137
156
|
|
|
@@ -148,6 +167,8 @@ const AGENTS_PATH =
|
|
|
148
167
|
const RUNTIME_PATH = /^\/api\/agents\/runtimes\/([^/]+)\/path$/;
|
|
149
168
|
const PERSONA_PATH = /^\/api\/personas\/([^/]+)$/;
|
|
150
169
|
const PLAIN_PATH = /^\/api\/workspaces\/([^/]+)\/sessions$/;
|
|
170
|
+
const CONVERSATIONS_PATH =
|
|
171
|
+
/^\/api\/workspaces\/([^/]+)\/conversations(?:\/([^/]+)\/import)?$/;
|
|
151
172
|
/** A start's body: a few names, commands and a first instruction. */
|
|
152
173
|
const MAX_BODY = 32 * 1024;
|
|
153
174
|
/** A list of names (a page's workspaces, an Undo's agents). */
|
|
@@ -166,6 +187,20 @@ function webDeps(over: AgentsDeps = {}): AgentsResolved {
|
|
|
166
187
|
if (!cfg) throw new AgentError(`workspace "${ws}" does not exist`);
|
|
167
188
|
return resolveWorkspaceDir(ws, cfg, getReposRoot());
|
|
168
189
|
},
|
|
190
|
+
// AG-17.1: its folder and each by-path repo's, outside it too. A caller
|
|
191
|
+
// that gives its own folder (a test) gives its folders too, or just it.
|
|
192
|
+
...(over.workspaceDir
|
|
193
|
+
? {}
|
|
194
|
+
: {
|
|
195
|
+
workspaceFolders: (ws: string) => {
|
|
196
|
+
const cfg = readWorkspacesFile().workspaces[ws];
|
|
197
|
+
if (!cfg) throw new AgentError(`workspace "${ws}" does not exist`);
|
|
198
|
+
return [
|
|
199
|
+
resolveWorkspaceDir(ws, cfg, getReposRoot()),
|
|
200
|
+
...repoFolders(cfg),
|
|
201
|
+
];
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
169
204
|
stdout: NO_TTY,
|
|
170
205
|
out: () => undefined,
|
|
171
206
|
err: () => undefined,
|
|
@@ -188,6 +223,7 @@ function sendError(res: http.ServerResponse, e: unknown, ws?: string): void {
|
|
|
188
223
|
error,
|
|
189
224
|
...(e.code && e.code !== NOT_IN_WORKSPACE ? { code: e.code } : {}),
|
|
190
225
|
...(e.hints.length ? { hints: e.hints } : {}),
|
|
226
|
+
...(e.running ? { running: e.running } : {}),
|
|
191
227
|
},
|
|
192
228
|
e.status,
|
|
193
229
|
);
|
|
@@ -275,6 +311,8 @@ interface StartBody {
|
|
|
275
311
|
permission?: string;
|
|
276
312
|
allow?: unknown;
|
|
277
313
|
instruction?: unknown;
|
|
314
|
+
/** AG-10.10: a display name, checked as a rename's is. */
|
|
315
|
+
name?: unknown;
|
|
278
316
|
}
|
|
279
317
|
|
|
280
318
|
const START_KEYS = [
|
|
@@ -286,6 +324,7 @@ const START_KEYS = [
|
|
|
286
324
|
"permission",
|
|
287
325
|
"allow",
|
|
288
326
|
"instruction",
|
|
327
|
+
"name",
|
|
289
328
|
];
|
|
290
329
|
|
|
291
330
|
async function startBody(req: http.IncomingMessage): Promise<StartBody> {
|
|
@@ -331,6 +370,8 @@ async function startBody(req: http.IncomingMessage): Promise<StartBody> {
|
|
|
331
370
|
}
|
|
332
371
|
if (o.allow !== undefined && o.allow !== null) out.allow = o.allow;
|
|
333
372
|
if (o.instruction !== undefined) out.instruction = o.instruction;
|
|
373
|
+
if (o.name !== undefined && o.name !== null && o.name !== "")
|
|
374
|
+
out.name = o.name;
|
|
334
375
|
return out as StartBody;
|
|
335
376
|
}
|
|
336
377
|
|
|
@@ -349,6 +390,7 @@ export async function handleAgentRoutes(
|
|
|
349
390
|
const rp = RUNTIME_PATH.exec(pathname);
|
|
350
391
|
const pp = PERSONA_PATH.exec(pathname);
|
|
351
392
|
const plain = PLAIN_PATH.exec(pathname);
|
|
393
|
+
const conv = CONVERSATIONS_PATH.exec(pathname);
|
|
352
394
|
// AG-14's routes over many agents at once.
|
|
353
395
|
const many =
|
|
354
396
|
pathname === "/api/agents/clear-stopped" ||
|
|
@@ -361,6 +403,7 @@ export async function handleAgentRoutes(
|
|
|
361
403
|
!rp &&
|
|
362
404
|
!pp &&
|
|
363
405
|
!plain &&
|
|
406
|
+
!conv &&
|
|
364
407
|
!many
|
|
365
408
|
)
|
|
366
409
|
return false;
|
|
@@ -369,6 +412,14 @@ export async function handleAgentRoutes(
|
|
|
369
412
|
|
|
370
413
|
if (pp) return personaRoute(req, res, safeDecode(pp[1]), d);
|
|
371
414
|
if (plain) return plainSession(req, res, safeDecode(plain[1]), d);
|
|
415
|
+
if (conv)
|
|
416
|
+
return conversations(
|
|
417
|
+
req,
|
|
418
|
+
res,
|
|
419
|
+
safeDecode(conv[1]),
|
|
420
|
+
conv[2] === undefined ? undefined : safeDecode(conv[2]),
|
|
421
|
+
d,
|
|
422
|
+
);
|
|
372
423
|
if (pathname === "/api/personas" && method === "POST")
|
|
373
424
|
return personaRoute(req, res, null, d);
|
|
374
425
|
|
|
@@ -387,6 +438,20 @@ export async function handleAgentRoutes(
|
|
|
387
438
|
...(pathname === "/api/personas"
|
|
388
439
|
? { personas: listPersonas(d), ...registries(d) }
|
|
389
440
|
: {}),
|
|
441
|
+
// AG-10.10: the id a new start of each persona takes here.
|
|
442
|
+
...(pathname === "/api/personas" && ws
|
|
443
|
+
? {
|
|
444
|
+
startsAs: Object.fromEntries(
|
|
445
|
+
listPersonas(d).flatMap((p) => {
|
|
446
|
+
try {
|
|
447
|
+
return [[p.id, defaultId(d.stateDir(), p.id, ws)]];
|
|
448
|
+
} catch {
|
|
449
|
+
return []; // no free id: the start says so
|
|
450
|
+
}
|
|
451
|
+
}),
|
|
452
|
+
),
|
|
453
|
+
}
|
|
454
|
+
: {}),
|
|
390
455
|
runtimes,
|
|
391
456
|
...(ws ? { workspace: ws } : {}),
|
|
392
457
|
// AG-0.2: the file a person edits to set a runtime's path.
|
|
@@ -509,6 +574,7 @@ export async function handleAgentRoutes(
|
|
|
509
574
|
? { permission: body.permission }
|
|
510
575
|
: {}),
|
|
511
576
|
...(body.allow !== undefined ? { allow: body.allow } : {}),
|
|
577
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
512
578
|
strict: true,
|
|
513
579
|
});
|
|
514
580
|
const ov = s.rec.overrides?.length
|
|
@@ -600,6 +666,91 @@ export async function handleAgentRoutes(
|
|
|
600
666
|
return true;
|
|
601
667
|
}
|
|
602
668
|
|
|
669
|
+
const IMPORT_KEYS = ["persona", "runtime", "id"];
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Bring in (AG-17.1): GET lists the workspace's conversations, POST
|
|
673
|
+
* …/<id>/import brings one in. A person's, or an agent's opted in through
|
|
674
|
+
* mcp.actions' agent_import inside its scope. Every import is audited.
|
|
675
|
+
*/
|
|
676
|
+
async function conversations(
|
|
677
|
+
req: http.IncomingMessage,
|
|
678
|
+
res: http.ServerResponse,
|
|
679
|
+
ws: string,
|
|
680
|
+
session: string | undefined,
|
|
681
|
+
d: AgentsResolved,
|
|
682
|
+
): Promise<true> {
|
|
683
|
+
const method = req.method ?? "GET";
|
|
684
|
+
const actor = auditActorOf(req);
|
|
685
|
+
const agent = agentPrincipalOf(req);
|
|
686
|
+
const allowed = () => {
|
|
687
|
+
if (agent) {
|
|
688
|
+
const why = agentImportRefusal(agent, ws, mcpActions());
|
|
689
|
+
if (why) throw new AgentError(why, { status: 403 });
|
|
690
|
+
} else guard(actor);
|
|
691
|
+
};
|
|
692
|
+
if (session === undefined) {
|
|
693
|
+
if (method !== "GET" && method !== "HEAD")
|
|
694
|
+
return methodNotAllowed(res, "GET");
|
|
695
|
+
try {
|
|
696
|
+
allowed();
|
|
697
|
+
const runtime =
|
|
698
|
+
new URL(req.url ?? "/", "http://x").searchParams.get("runtime") ??
|
|
699
|
+
undefined;
|
|
700
|
+
const listed = listConversations(d, ws, runtime);
|
|
701
|
+
sendJson(res, { workspace: ws, folder: d.workspaceDir(ws), ...listed });
|
|
702
|
+
} catch (e) {
|
|
703
|
+
sendError(res, e, ws);
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
if (method !== "POST") return methodNotAllowed(res, "POST");
|
|
708
|
+
let what = `bring in ${session} in ${ws}`;
|
|
709
|
+
const audit = (reason?: string) =>
|
|
710
|
+
recordAction(
|
|
711
|
+
actor,
|
|
712
|
+
{
|
|
713
|
+
verb: "agents",
|
|
714
|
+
detail: what,
|
|
715
|
+
...(reason ? { outcome: "refused" as const, reason } : {}),
|
|
716
|
+
},
|
|
717
|
+
principalOf(req),
|
|
718
|
+
);
|
|
719
|
+
try {
|
|
720
|
+
allowed();
|
|
721
|
+
const body = await jsonBody(req);
|
|
722
|
+
for (const k of Object.keys(body))
|
|
723
|
+
if (!IMPORT_KEYS.includes(k))
|
|
724
|
+
throw new AgentError(
|
|
725
|
+
`${k}: unknown field (an import takes ${IMPORT_KEYS.join(", ")})`,
|
|
726
|
+
{ usage: true },
|
|
727
|
+
);
|
|
728
|
+
const str = (k: string): string | undefined => {
|
|
729
|
+
const v = body[k];
|
|
730
|
+
if (v === undefined || v === null || v === "") return undefined;
|
|
731
|
+
if (typeof v !== "string" || !NAME_RE.test(v))
|
|
732
|
+
throw new AgentError(`${k} must be a name`, { usage: true });
|
|
733
|
+
return v;
|
|
734
|
+
};
|
|
735
|
+
const persona = str("persona");
|
|
736
|
+
if (!persona) throw new AgentError("persona is required", { usage: true });
|
|
737
|
+
const s = await importAgent(d, {
|
|
738
|
+
workspace: ws,
|
|
739
|
+
session,
|
|
740
|
+
persona,
|
|
741
|
+
runtime: str("runtime"),
|
|
742
|
+
id: str("id"),
|
|
743
|
+
});
|
|
744
|
+
what = `bring in ${session} as ${s.rec.id} (${s.rec.persona}, ${s.rec.runtime}) in ${ws}`;
|
|
745
|
+
audit(s.early ? "exited at once" : undefined);
|
|
746
|
+
sendStarted(res, s, d);
|
|
747
|
+
} catch (e) {
|
|
748
|
+
audit(e instanceof Error ? e.message : String(e));
|
|
749
|
+
sendError(res, e, ws);
|
|
750
|
+
}
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
603
754
|
/** The org config's mcp.actions; none when it can't be read. */
|
|
604
755
|
function mcpActions(): readonly string[] {
|
|
605
756
|
try {
|
|
@@ -1535,8 +1535,99 @@ const sd = {
|
|
|
1535
1535
|
intent: "ask",
|
|
1536
1536
|
extras: [],
|
|
1537
1537
|
extrasOpen: false,
|
|
1538
|
+
// AG-17.1, Bring in: null for Start; else the workspace's conversations,
|
|
1539
|
+
// the runtimes that can't bring one in, and the one picked.
|
|
1540
|
+
bring: null,
|
|
1538
1541
|
};
|
|
1539
1542
|
|
|
1543
|
+
/** The conversation picked in Bring in, when it can be brought in. */
|
|
1544
|
+
function bringPick() {
|
|
1545
|
+
const c = (sd.bring?.list ?? []).find((x) => x.id === sd.bring.pick);
|
|
1546
|
+
return c && !c.open && !c.agent ? c : null;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/** "3 min ago", "2 h ago", or the date. */
|
|
1550
|
+
function agoText(iso) {
|
|
1551
|
+
const ms = Date.now() - Date.parse(iso);
|
|
1552
|
+
if (!Number.isFinite(ms)) return "";
|
|
1553
|
+
const m = Math.round(ms / 60000);
|
|
1554
|
+
if (m < 1) return t("ag.bring.now");
|
|
1555
|
+
if (m < 60) return t("ag.bring.min", { n: m });
|
|
1556
|
+
const h = Math.round(m / 60);
|
|
1557
|
+
if (h < 48) return t("ag.bring.h", { n: h });
|
|
1558
|
+
return iso.slice(0, 10);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/** The folder a conversation ran in, inside the workspace's when it is. */
|
|
1562
|
+
function convFolder(c) {
|
|
1563
|
+
const root = sd.bring?.root;
|
|
1564
|
+
if (root && c.cwd === root) return ".";
|
|
1565
|
+
if (root && c.cwd.startsWith(`${root}/`)) return c.cwd.slice(root.length + 1);
|
|
1566
|
+
return c.cwd;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function convRowHtml(c) {
|
|
1570
|
+
const taken = c.agent
|
|
1571
|
+
? ` <span class="tagx">${esc(t("ag.bring.agent", { id: c.agent }))}</span>`
|
|
1572
|
+
: c.open
|
|
1573
|
+
? ` <span class="tagx warn" title="${esc(t("ag.bring.openTip"))}">${esc(t("ag.bring.open"))}</span>`
|
|
1574
|
+
: "";
|
|
1575
|
+
const off = c.agent || c.open;
|
|
1576
|
+
return (
|
|
1577
|
+
`<button type="button" class="pr" role="radio" data-cid="${esc(c.id)}" aria-checked="${c.id === sd.bring.pick}"${off ? ' aria-disabled="true"' : ""}>` +
|
|
1578
|
+
`<span class="rd" aria-hidden="true"></span><span class="pn pnc"><span class="pt">${esc(c.title || t("ag.bring.untitled"))}</span>${taken}</span><span class="mono">${esc(agoText(c.lastAt))}</span>` +
|
|
1579
|
+
`<span class="pd mono">${esc(convFolder(c))} · ${esc(c.id.slice(0, 8))}</span></button>`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/** Bring in's conversation list and its help line. */
|
|
1584
|
+
function paintConversations() {
|
|
1585
|
+
const f = $d("#ag-conv-f");
|
|
1586
|
+
f.hidden = !sd.bring;
|
|
1587
|
+
if (!sd.bring) return;
|
|
1588
|
+
const list = sd.bring.list;
|
|
1589
|
+
const cant = (sd.bring.runtimes ?? [])
|
|
1590
|
+
.filter((r) => !r.bringIn && r.id !== "generic")
|
|
1591
|
+
.map((r) => r.why);
|
|
1592
|
+
$d("#ag-conv-h").textContent = [
|
|
1593
|
+
list.length ? t("ag.bring.help") : t("ag.bring.none"),
|
|
1594
|
+
// Each its own sentence (they read as one run-on line before).
|
|
1595
|
+
...cant.map((w) => (/[.!?]$/.test(w) ? w : `${w}.`)),
|
|
1596
|
+
].join(" ");
|
|
1597
|
+
$d("#ag-conv").innerHTML = list.map(convRowHtml).join("");
|
|
1598
|
+
$d("#ag-conv").hidden = !list.length;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/** Ask for the workspace's conversations (Bring in). */
|
|
1602
|
+
async function fillConversations(ws) {
|
|
1603
|
+
if (!sd.bring) return true;
|
|
1604
|
+
try {
|
|
1605
|
+
const r = await api(
|
|
1606
|
+
"GET",
|
|
1607
|
+
`/api/workspaces/${encodeURIComponent(ws)}/conversations`,
|
|
1608
|
+
);
|
|
1609
|
+
sd.bring.list = r?.conversations ?? [];
|
|
1610
|
+
sd.bring.runtimes = r?.runtimes ?? [];
|
|
1611
|
+
sd.bring.root = r?.folder ?? "";
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
window.denShell?.errorToast?.(err);
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
if (!sd.bring.list.some((c) => c.id === sd.bring.pick))
|
|
1617
|
+
sd.bring.pick = sd.bring.list.find((c) => !c.open && !c.agent)?.id ?? "";
|
|
1618
|
+
paintConversations();
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function pickConversation(id) {
|
|
1623
|
+
const c = (sd.bring?.list ?? []).find((x) => x.id === id);
|
|
1624
|
+
if (!c || c.open || c.agent) return;
|
|
1625
|
+
sd.bring.pick = id;
|
|
1626
|
+
for (const b of st.root.querySelectorAll("#ag-conv [data-cid]"))
|
|
1627
|
+
b.setAttribute("aria-checked", String(b.dataset.cid === id));
|
|
1628
|
+
paintRuntime(false);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1540
1631
|
const $d = (sel) => st.root.querySelector(sel);
|
|
1541
1632
|
const curPersona = () =>
|
|
1542
1633
|
sd.pid === PLAIN
|
|
@@ -1563,10 +1654,13 @@ function personaModel() {
|
|
|
1563
1654
|
function plistHtml() {
|
|
1564
1655
|
const ps = sd.data?.personas ?? [];
|
|
1565
1656
|
const cats = sd.data?.categories ?? [];
|
|
1657
|
+
const rows = personaListHtml(sd.f, ps, cats, (p) =>
|
|
1658
|
+
personaRowHtml(p, p.id === sd.pid, sd.data),
|
|
1659
|
+
);
|
|
1660
|
+
// Bring in continues a conversation: it needs a persona.
|
|
1661
|
+
if (sd.bring) return rows;
|
|
1566
1662
|
return (
|
|
1567
|
-
|
|
1568
|
-
personaRowHtml(p, p.id === sd.pid, sd.data),
|
|
1569
|
-
) +
|
|
1663
|
+
rows +
|
|
1570
1664
|
`<div class="pgh"><span>${esc(t("ag.session"))}</span><span></span></div>` +
|
|
1571
1665
|
`<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
1666
|
);
|
|
@@ -1612,9 +1706,21 @@ function paintRuntime(reset) {
|
|
|
1612
1706
|
const go = $d("#ag-go");
|
|
1613
1707
|
const ready = $d("#ag-r-ready");
|
|
1614
1708
|
const plain = sd.pid === PLAIN;
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
$d("#ag-
|
|
1709
|
+
// Bring in: the conversation's runtime, its own history; no first
|
|
1710
|
+
// instruction, no Advanced (it keeps what the persona gives).
|
|
1711
|
+
$d("#ag-r-l").hidden = plain || !!sd.bring;
|
|
1712
|
+
$d("#ag-task-l").hidden = plain || !!sd.bring;
|
|
1713
|
+
$d("#ag-adv").hidden = plain || !!sd.bring;
|
|
1714
|
+
$d("#ag-name-l").hidden = plain || !!sd.bring;
|
|
1715
|
+
paintStartsAs();
|
|
1716
|
+
if (sd.bring) {
|
|
1717
|
+
$d("#ag-r-miss").hidden = true;
|
|
1718
|
+
ready.hidden = true;
|
|
1719
|
+
const ok = !!bringPick() && !!curPersona();
|
|
1720
|
+
go.disabled = !ok;
|
|
1721
|
+
go.dataset.blocked = ok ? "" : "conversation";
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1618
1724
|
const fb = $d("#ag-r-fb");
|
|
1619
1725
|
fb.hidden = true;
|
|
1620
1726
|
if (plain) {
|
|
@@ -1675,7 +1781,10 @@ function paintAdvanced() {
|
|
|
1675
1781
|
const models = rtv?.models ?? [];
|
|
1676
1782
|
const model = sd.model || def.model;
|
|
1677
1783
|
const first = t("ag.new.modelDef", {
|
|
1678
|
-
|
|
1784
|
+
// No model of its own: just the version, so the label fits the select.
|
|
1785
|
+
m: def.model
|
|
1786
|
+
? `${def.model} · ${versionLabel(def.version)}`
|
|
1787
|
+
: versionLabel("latest"),
|
|
1679
1788
|
});
|
|
1680
1789
|
const verFirst = t("ag.new.verDef", {
|
|
1681
1790
|
v: versionLabel(
|
|
@@ -1749,6 +1858,8 @@ async function fillForWorkspace(ws) {
|
|
|
1749
1858
|
window.denShell?.errorToast?.(err);
|
|
1750
1859
|
return false;
|
|
1751
1860
|
}
|
|
1861
|
+
if (!(await fillConversations(ws))) return false;
|
|
1862
|
+
if (sd.bring && sd.pid === PLAIN) sd.pid = "";
|
|
1752
1863
|
const ps = sd.data?.personas ?? [];
|
|
1753
1864
|
if (sd.pid !== PLAIN && !ps.some((p) => p.id === sd.pid)) {
|
|
1754
1865
|
// The first persona the list shows (grouped by category).
|
|
@@ -1765,9 +1876,19 @@ async function fillForWorkspace(ws) {
|
|
|
1765
1876
|
return true;
|
|
1766
1877
|
}
|
|
1767
1878
|
|
|
1768
|
-
|
|
1879
|
+
/** The dialog's words for Start, or for Bring in (AG-17.1). */
|
|
1880
|
+
function dialogMode(bring) {
|
|
1881
|
+
sd.bring = bring ? { list: [], runtimes: [], root: "", pick: "" } : null;
|
|
1882
|
+
$d("#ag-dlg-h").textContent = t(bring ? "ag.bring.title" : "ag.new.title");
|
|
1883
|
+
$d("#ag-note").textContent = t(bring ? "ag.bring.note" : "ag.new.note");
|
|
1884
|
+
$d("#ag-go").textContent = t(bring ? "ag.bring.go" : "ag.go");
|
|
1885
|
+
$d("#ag-conv-f").hidden = !bring;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
async function openStart(bring = false) {
|
|
1769
1889
|
const dlg = $d("#ag-dlg");
|
|
1770
1890
|
if (!dlg || dlg.open) return;
|
|
1891
|
+
dialogMode(bring);
|
|
1771
1892
|
const shown = viewWorkspaces();
|
|
1772
1893
|
const wsSel = $d("#ag-ws");
|
|
1773
1894
|
wsSel.innerHTML = shown
|
|
@@ -1812,21 +1933,48 @@ function startBody() {
|
|
|
1812
1933
|
}
|
|
1813
1934
|
const task = $d("#ag-task").value.trim();
|
|
1814
1935
|
if (task) body.instruction = task;
|
|
1936
|
+
const name = $d("#ag-name").value.trim();
|
|
1937
|
+
if (name) body.name = name;
|
|
1815
1938
|
return body;
|
|
1816
1939
|
}
|
|
1817
1940
|
|
|
1818
1941
|
function showStartError(err) {
|
|
1819
1942
|
const el = $d("#ag-err");
|
|
1820
1943
|
el.textContent = err?.message || String(err);
|
|
1944
|
+
// AG-10.10: that agent is running: offer to open it.
|
|
1945
|
+
const running = err?.data?.running;
|
|
1946
|
+
if (running?.sessionId) {
|
|
1947
|
+
const b = document.createElement("button");
|
|
1948
|
+
b.type = "button";
|
|
1949
|
+
b.className = "den-btn sm";
|
|
1950
|
+
b.id = "ag-open-running";
|
|
1951
|
+
b.textContent = t("ag.new.openRunning");
|
|
1952
|
+
b.addEventListener("click", () => {
|
|
1953
|
+
$d("#ag-dlg").close?.();
|
|
1954
|
+
void openPane(running.sessionId);
|
|
1955
|
+
});
|
|
1956
|
+
el.append(" ", b);
|
|
1957
|
+
}
|
|
1821
1958
|
el.hidden = false;
|
|
1822
1959
|
}
|
|
1823
1960
|
|
|
1961
|
+
/** AG-10.10: the id this start takes, so two of one persona are told apart. */
|
|
1962
|
+
function paintStartsAs() {
|
|
1963
|
+
const el = $d("#ag-as");
|
|
1964
|
+
if (!el) return;
|
|
1965
|
+
const p = sd.pid && sd.pid !== PLAIN && !sd.bring ? curPersona() : null;
|
|
1966
|
+
el.hidden = !p;
|
|
1967
|
+
if (p)
|
|
1968
|
+
el.textContent = t("ag.new.as", { id: sd.data?.startsAs?.[p.id] ?? p.id });
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1824
1971
|
async function submitStart(form) {
|
|
1825
1972
|
const dlg = $d("#ag-dlg");
|
|
1826
1973
|
const go = $d("#ag-go");
|
|
1827
1974
|
if (go.disabled) return;
|
|
1828
1975
|
const ws = form.querySelector("#ag-ws").value || viewWorkspaces()[0];
|
|
1829
1976
|
if (!ws || !sd.pid) return;
|
|
1977
|
+
if (sd.bring) return submitBringIn(ws);
|
|
1830
1978
|
const bad = sd.extras.find((c) => commandProblem(c));
|
|
1831
1979
|
if (sd.pid !== PLAIN && bad)
|
|
1832
1980
|
return showStartError(new Error(t("pe.badCmd", { c: bad })));
|
|
@@ -1852,6 +2000,9 @@ async function submitStart(form) {
|
|
|
1852
2000
|
startBody(),
|
|
1853
2001
|
);
|
|
1854
2002
|
window.denShell?.toast?.(t("ag.started", { id: r?.agent?.id ?? sd.pid }));
|
|
2003
|
+
// AG-10.10: the next start of this persona takes another id.
|
|
2004
|
+
personasAsked.delete(ws);
|
|
2005
|
+
$d("#ag-name").value = "";
|
|
1855
2006
|
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
1856
2007
|
dlg.close?.();
|
|
1857
2008
|
await refresh();
|
|
@@ -1865,6 +2016,36 @@ async function submitStart(form) {
|
|
|
1865
2016
|
}
|
|
1866
2017
|
}
|
|
1867
2018
|
|
|
2019
|
+
/** Bring in (AG-17.1): the runtime's resume of the conversation picked. */
|
|
2020
|
+
async function submitBringIn(ws) {
|
|
2021
|
+
const dlg = $d("#ag-dlg");
|
|
2022
|
+
const go = $d("#ag-go");
|
|
2023
|
+
const c = bringPick();
|
|
2024
|
+
const p = curPersona();
|
|
2025
|
+
if (!c || !p) return;
|
|
2026
|
+
go.disabled = true;
|
|
2027
|
+
go.textContent = t("ag.bring.going");
|
|
2028
|
+
try {
|
|
2029
|
+
const r = await api(
|
|
2030
|
+
"POST",
|
|
2031
|
+
`/api/workspaces/${encodeURIComponent(ws)}/conversations/${encodeURIComponent(c.id)}/import`,
|
|
2032
|
+
{ persona: p.id, runtime: c.runtime },
|
|
2033
|
+
);
|
|
2034
|
+
window.denShell?.toast?.(t("ag.bring.done", { id: r?.agent?.id ?? p.id }));
|
|
2035
|
+
for (const w of r?.warnings ?? []) window.denShell?.toast?.(w);
|
|
2036
|
+
dlg.close?.();
|
|
2037
|
+
await refresh();
|
|
2038
|
+
if (r?.agent?.sessionId) await openPane(r.agent.sessionId);
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
showStartError(err);
|
|
2041
|
+
// Still open elsewhere, or taken since: the list says so now.
|
|
2042
|
+
await fillConversations(ws);
|
|
2043
|
+
} finally {
|
|
2044
|
+
go.textContent = t("ag.bring.go");
|
|
2045
|
+
if (dlg.open) paintRuntime(false);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1868
2049
|
/** New persona, Edit or Duplicate from the dialog; back to it after. */
|
|
1869
2050
|
async function editFromStart(kind) {
|
|
1870
2051
|
const p = curPersona();
|
|
@@ -1892,6 +2073,12 @@ async function editFromStart(kind) {
|
|
|
1892
2073
|
function wireStartDialog(root) {
|
|
1893
2074
|
const form = root.querySelector("#ag-form");
|
|
1894
2075
|
form.addEventListener("click", (e) => {
|
|
2076
|
+
const cr = e.target.closest("#ag-conv [data-cid]");
|
|
2077
|
+
if (cr) {
|
|
2078
|
+
e.preventDefault();
|
|
2079
|
+
pickConversation(cr.dataset.cid);
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
1895
2082
|
const pr = e.target.closest("#ag-persona [data-pid]");
|
|
1896
2083
|
if (pr) {
|
|
1897
2084
|
e.preventDefault();
|
|
@@ -2032,6 +2219,8 @@ function translateStatic(root) {
|
|
|
2032
2219
|
};
|
|
2033
2220
|
set("#ag-h", "n.agents");
|
|
2034
2221
|
set("#ag-start", "ag.start");
|
|
2222
|
+
set("#ag-bring", "ag.bring");
|
|
2223
|
+
set("#ag-conv-t", "ag.bring.conv");
|
|
2035
2224
|
set("#ag-clear", "ag.clear");
|
|
2036
2225
|
set("#ag-dlg-h", "ag.new.title");
|
|
2037
2226
|
set("#ag-ws-t", "ag.ws");
|
|
@@ -2041,6 +2230,7 @@ function translateStatic(root) {
|
|
|
2041
2230
|
set("#ag-newp", "ag.new.newP");
|
|
2042
2231
|
set("#ag-what-p", "pe.what");
|
|
2043
2232
|
set("#ag-task-t", "ag.new.task");
|
|
2233
|
+
set("#ag-name-t", "ag.new.name");
|
|
2044
2234
|
set("#ag-adv-s", "ag.new.adv");
|
|
2045
2235
|
set("#ag-note", "ag.new.note");
|
|
2046
2236
|
const task = root.querySelector("#ag-task");
|
|
@@ -2129,6 +2319,9 @@ export async function mountAgentsPage(root) {
|
|
|
2129
2319
|
root.querySelector("#ag-start").addEventListener("click", () => {
|
|
2130
2320
|
void openStart();
|
|
2131
2321
|
});
|
|
2322
|
+
root.querySelector("#ag-bring")?.addEventListener("click", () => {
|
|
2323
|
+
void openStart(true);
|
|
2324
|
+
});
|
|
2132
2325
|
root.querySelector("#ag-clear")?.addEventListener("click", (e) => {
|
|
2133
2326
|
void clearStopped(e.currentTarget);
|
|
2134
2327
|
});
|
|
@@ -642,6 +642,7 @@
|
|
|
642
642
|
<button type="button" class="den-btn" id="ag-clear" hidden>
|
|
643
643
|
Clear stopped
|
|
644
644
|
</button>
|
|
645
|
+
<button type="button" class="den-btn" id="ag-bring">Bring in</button>
|
|
645
646
|
<button type="button" class="den-btn primary" id="ag-start">
|
|
646
647
|
+ Start agent
|
|
647
648
|
</button>
|
|
@@ -660,6 +661,18 @@
|
|
|
660
661
|
<dialog class="ag-dlg pe-dlg" id="ag-dlg" aria-labelledby="ag-dlg-h">
|
|
661
662
|
<form method="dialog" id="ag-form" novalidate>
|
|
662
663
|
<h2 id="ag-dlg-h">Start an agent</h2>
|
|
664
|
+
<div class="fld" id="ag-conv-f" hidden>
|
|
665
|
+
<div class="fldh">
|
|
666
|
+
<span class="k" id="ag-conv-t">Conversation</span>
|
|
667
|
+
</div>
|
|
668
|
+
<p class="helpl" id="ag-conv-h"></p>
|
|
669
|
+
<div
|
|
670
|
+
class="plist"
|
|
671
|
+
id="ag-conv"
|
|
672
|
+
role="radiogroup"
|
|
673
|
+
aria-labelledby="ag-conv-t"
|
|
674
|
+
></div>
|
|
675
|
+
</div>
|
|
663
676
|
<div class="fld" id="ag-pfld">
|
|
664
677
|
<div class="fldh">
|
|
665
678
|
<span class="k" id="ag-p-t">Persona</span
|
|
@@ -712,6 +725,11 @@
|
|
|
712
725
|
><span id="ag-task-t">First instruction (optional)</span>
|
|
713
726
|
<textarea id="ag-task" rows="4" maxlength="8000"></textarea>
|
|
714
727
|
</label>
|
|
728
|
+
<label class="fld" id="ag-name-l"
|
|
729
|
+
><span id="ag-name-t">Name (optional)</span>
|
|
730
|
+
<input id="ag-name" maxlength="40" autocomplete="off" />
|
|
731
|
+
</label>
|
|
732
|
+
<p class="helpl" id="ag-as" role="status"></p>
|
|
715
733
|
<details id="ag-adv">
|
|
716
734
|
<summary id="ag-adv-s">Advanced: model and permissions</summary>
|
|
717
735
|
<div class="adv-body" id="ag-adv-b"></div>
|
|
@@ -456,6 +456,9 @@
|
|
|
456
456
|
"ag.new.plainP":
|
|
457
457
|
"Una terminal en la carpeta del espacio de trabajo. Sin agente.",
|
|
458
458
|
"ag.new.task": "Primera instrucción (opcional)",
|
|
459
|
+
"ag.new.name": "Nombre (opcional)",
|
|
460
|
+
"ag.new.as": "Se inicia como {id}",
|
|
461
|
+
"ag.new.openRunning": "Abrir el que está en ejecución",
|
|
459
462
|
"ag.new.taskPh":
|
|
460
463
|
"p. ej., Toma la historia del estado vacío. Sigue el diseño y añade una prueba.",
|
|
461
464
|
"ag.new.adv": "Avanzado: modelo y permisos",
|
|
@@ -465,6 +468,26 @@
|
|
|
465
468
|
"ag.new.modelDef": "Predeterminado de la persona ({m})",
|
|
466
469
|
"ag.new.ver": "Versión",
|
|
467
470
|
"ag.new.verDef": "Predeterminado de la persona ({v})",
|
|
471
|
+
// AG-17.1, Bring in.
|
|
472
|
+
"ag.bring": "Traer",
|
|
473
|
+
"ag.bring.title": "Traer una conversación",
|
|
474
|
+
"ag.bring.conv": "Conversación",
|
|
475
|
+
"ag.bring.help":
|
|
476
|
+
"Conversaciones iniciadas fuera de Den en la carpeta de este espacio de trabajo o en las de sus repositorios, la más reciente primero.",
|
|
477
|
+
"ag.bring.none":
|
|
478
|
+
"No hay conversaciones iniciadas fuera de Den en la carpeta de este espacio de trabajo ni en las de sus repositorios.",
|
|
479
|
+
"ag.bring.untitled": "Conversación sin título",
|
|
480
|
+
"ag.bring.open": "Abierta en otro lugar",
|
|
481
|
+
"ag.bring.openTip": "Sigue abierta en otra terminal. Ciérrala allí primero",
|
|
482
|
+
"ag.bring.agent": "Agente {id}",
|
|
483
|
+
"ag.bring.note":
|
|
484
|
+
"La continúa en la carpeta donde se ejecutó, con las herramientas y protecciones de Den desde ahora. Sigue en ejecución si cierras el panel. Ciérrala antes en cualquier otro terminal: Den solo la ve abierta si se escribió en el último minuto.",
|
|
485
|
+
"ag.bring.go": "Traer",
|
|
486
|
+
"ag.bring.going": "Trayendo…",
|
|
487
|
+
"ag.bring.done": "Traída como {id}",
|
|
488
|
+
"ag.bring.now": "ahora mismo",
|
|
489
|
+
"ag.bring.min": "hace {n} min",
|
|
490
|
+
"ag.bring.h": "hace {n} h",
|
|
468
491
|
"ag.new.note":
|
|
469
492
|
"Se inicia en la carpeta del espacio de trabajo. Sigue en ejecución si cierras el panel.",
|
|
470
493
|
"ag.new.started": "{p} iniciado en {w}",
|