@hueyexe/opencode-ensemble 0.9.1 → 0.11.0
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/README.md +33 -5
- package/dist/index.js +715 -3
- package/package.json +1 -1
- package/src/config.ts +12 -0
- package/src/dashboard-html.ts +91 -0
- package/src/dashboard-js-part1.ts +97 -0
- package/src/dashboard-js-part2.ts +233 -0
- package/src/dashboard-js-part3.ts +50 -0
- package/src/dashboard.ts +166 -0
- package/src/index.ts +35 -0
- package/src/progress.ts +38 -0
- package/src/system-prompt.ts +28 -2
- package/src/tools/team-message.ts +16 -0
- package/src/tools/team-spawn.ts +13 -0
- package/src/watchdog.ts +44 -0
package/dist/index.js
CHANGED
|
@@ -15987,7 +15987,28 @@ function buildTeammateSystemPrompt(db, teamId, memberName) {
|
|
|
15987
15987
|
const team = db.query("SELECT name FROM team WHERE id = ?").get(teamId);
|
|
15988
15988
|
if (!team)
|
|
15989
15989
|
return "";
|
|
15990
|
-
|
|
15990
|
+
const lines = [
|
|
15991
|
+
`You are "${memberName}", a teammate in team "${team.name}". Use team_message to communicate. You MUST send your results to the lead via team_message before stopping.`
|
|
15992
|
+
];
|
|
15993
|
+
const pendingMessages = db.query("SELECT id, from_name, content FROM team_message WHERE team_id = ? AND to_name = ? AND delivered = 0 ORDER BY time_created ASC").all(teamId, memberName);
|
|
15994
|
+
if (pendingMessages.length > 0) {
|
|
15995
|
+
lines.push("", "--- Messages for you ---");
|
|
15996
|
+
const MAX_MSG = 500;
|
|
15997
|
+
for (const msg of pendingMessages) {
|
|
15998
|
+
const parsed = parseTaskResult(msg.content);
|
|
15999
|
+
if (parsed) {
|
|
16000
|
+
lines.push(formatTaskResult(msg.from_name, { ...parsed, details: truncate(parsed.details, MAX_MSG) }));
|
|
16001
|
+
} else if (msg.content.length > MAX_MSG) {
|
|
16002
|
+
lines.push(`[From ${msg.from_name}]: ${msg.content.slice(0, MAX_MSG)}... (truncated)`);
|
|
16003
|
+
} else {
|
|
16004
|
+
lines.push(`[From ${msg.from_name}]: ${msg.content}`);
|
|
16005
|
+
}
|
|
16006
|
+
markDelivered(db, msg.id);
|
|
16007
|
+
}
|
|
16008
|
+
lines.push("--- End Messages ---");
|
|
16009
|
+
}
|
|
16010
|
+
return lines.join(`
|
|
16011
|
+
`);
|
|
15991
16012
|
}
|
|
15992
16013
|
function buildTeamCompactionContext(db, teamId, role, memberName) {
|
|
15993
16014
|
const team = db.query("SELECT name FROM team WHERE id = ?").get(teamId);
|
|
@@ -16069,7 +16090,10 @@ var DEFAULT_CONFIG = {
|
|
|
16069
16090
|
stallMinSteps: 3,
|
|
16070
16091
|
stallTokenThreshold: 500,
|
|
16071
16092
|
timeoutMs: 30 * 60 * 1000,
|
|
16072
|
-
rateLimitCapacity: 10
|
|
16093
|
+
rateLimitCapacity: 10,
|
|
16094
|
+
dashboardPort: 4747,
|
|
16095
|
+
peerMessageLimit: 5,
|
|
16096
|
+
peerMessageWindowMs: 300000
|
|
16073
16097
|
};
|
|
16074
16098
|
function readConfigFile(filePath) {
|
|
16075
16099
|
try {
|
|
@@ -16088,6 +16112,12 @@ function readConfigFile(filePath) {
|
|
|
16088
16112
|
result.timeoutMs = raw.timeoutMs;
|
|
16089
16113
|
if (typeof raw.rateLimitCapacity === "number")
|
|
16090
16114
|
result.rateLimitCapacity = raw.rateLimitCapacity;
|
|
16115
|
+
if (typeof raw.dashboardPort === "number")
|
|
16116
|
+
result.dashboardPort = raw.dashboardPort;
|
|
16117
|
+
if (typeof raw.peerMessageLimit === "number")
|
|
16118
|
+
result.peerMessageLimit = raw.peerMessageLimit;
|
|
16119
|
+
if (typeof raw.peerMessageWindowMs === "number")
|
|
16120
|
+
result.peerMessageWindowMs = raw.peerMessageWindowMs;
|
|
16091
16121
|
return result;
|
|
16092
16122
|
} catch (err) {
|
|
16093
16123
|
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT")
|
|
@@ -16120,7 +16150,9 @@ class ProgressTracker {
|
|
|
16120
16150
|
steps = new Map;
|
|
16121
16151
|
lastMessageAt = new Map;
|
|
16122
16152
|
lastTaskAt = new Map;
|
|
16153
|
+
peerMessages = new Map;
|
|
16123
16154
|
reported = new Set;
|
|
16155
|
+
chattyReported = new Set;
|
|
16124
16156
|
maxSteps;
|
|
16125
16157
|
constructor(maxSteps = 10) {
|
|
16126
16158
|
this.maxSteps = maxSteps;
|
|
@@ -16132,6 +16164,31 @@ class ProgressTracker {
|
|
|
16132
16164
|
records.shift();
|
|
16133
16165
|
this.steps.set(sessionId, records);
|
|
16134
16166
|
}
|
|
16167
|
+
recordPeerMessage(sessionId) {
|
|
16168
|
+
const timestamps = this.peerMessages.get(sessionId) ?? [];
|
|
16169
|
+
timestamps.push(Date.now());
|
|
16170
|
+
this.peerMessages.set(sessionId, timestamps);
|
|
16171
|
+
}
|
|
16172
|
+
isChatty(sessionId, limit, windowMs) {
|
|
16173
|
+
if (limit === 0)
|
|
16174
|
+
return false;
|
|
16175
|
+
const timestamps = this.peerMessages.get(sessionId);
|
|
16176
|
+
if (!timestamps)
|
|
16177
|
+
return false;
|
|
16178
|
+
const cutoff = Date.now() - windowMs;
|
|
16179
|
+
const recent = timestamps.filter((t) => t >= cutoff);
|
|
16180
|
+
this.peerMessages.set(sessionId, recent);
|
|
16181
|
+
return recent.length >= limit;
|
|
16182
|
+
}
|
|
16183
|
+
markChattyReported(sessionId) {
|
|
16184
|
+
this.chattyReported.add(sessionId);
|
|
16185
|
+
}
|
|
16186
|
+
isChattyReported(sessionId) {
|
|
16187
|
+
return this.chattyReported.has(sessionId);
|
|
16188
|
+
}
|
|
16189
|
+
clearChattyReport(sessionId) {
|
|
16190
|
+
this.chattyReported.delete(sessionId);
|
|
16191
|
+
}
|
|
16135
16192
|
recordMessage(sessionId) {
|
|
16136
16193
|
this.lastMessageAt.set(sessionId, Date.now());
|
|
16137
16194
|
this.clearReport(sessionId);
|
|
@@ -16170,7 +16227,590 @@ class ProgressTracker {
|
|
|
16170
16227
|
this.steps.delete(sessionId);
|
|
16171
16228
|
this.lastMessageAt.delete(sessionId);
|
|
16172
16229
|
this.lastTaskAt.delete(sessionId);
|
|
16230
|
+
this.peerMessages.delete(sessionId);
|
|
16173
16231
|
this.reported.delete(sessionId);
|
|
16232
|
+
this.chattyReported.delete(sessionId);
|
|
16233
|
+
}
|
|
16234
|
+
}
|
|
16235
|
+
|
|
16236
|
+
// src/dashboard-html.ts
|
|
16237
|
+
var DASHBOARD_HEAD = `<!DOCTYPE html>
|
|
16238
|
+
<html lang="en">
|
|
16239
|
+
<head>
|
|
16240
|
+
<meta charset="UTF-8">
|
|
16241
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
16242
|
+
<title>Ensemble</title>
|
|
16243
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><circle cx='8' cy='8' r='6' fill='%2322c55e'/></svg>">
|
|
16244
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
16245
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16246
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
16247
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
16248
|
+
<script>tailwind.config={theme:{extend:{colors:{base:{950:'#0c0e14',900:'#141822',850:'#1a1f2e',800:'#1e2433',700:'#2a3144',600:'#3a4358'},txt:{100:'#e2e8f0',200:'#c1c9d9',300:'#8892a8',400:'#5e6a82',500:'#4a5568'}},fontFamily:{sans:['Inter','system-ui','sans-serif'],mono:['JetBrains Mono','monospace']}}}}</script>
|
|
16249
|
+
<style>
|
|
16250
|
+
@media(prefers-reduced-motion:no-preference){
|
|
16251
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
16252
|
+
@keyframes fadein{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
|
|
16253
|
+
@keyframes hl{from{background:rgba(59,130,246,.1)}to{background:transparent}}
|
|
16254
|
+
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
|
|
16255
|
+
.pulse{animation:pulse 2s ease-in-out infinite}
|
|
16256
|
+
.fadein{animation:fadein .25s ease-out}
|
|
16257
|
+
.hl{animation:hl 1.5s ease-out}
|
|
16258
|
+
.shimmer{background:linear-gradient(90deg,#22c55e 0%,#4ade80 50%,#22c55e 100%)!important;background-size:200% 100%;animation:shimmer 1.5s ease-in-out}
|
|
16259
|
+
}
|
|
16260
|
+
.scroll::-webkit-scrollbar{width:5px}.scroll::-webkit-scrollbar-track{background:transparent}.scroll::-webkit-scrollbar-thumb{background:#2a3144;border-radius:3px}
|
|
16261
|
+
details summary::-webkit-details-marker{display:none}details summary{list-style:none}
|
|
16262
|
+
select{-webkit-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%235e6a82' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:22px}
|
|
16263
|
+
.xp{max-height:0;overflow:hidden;transition:max-height .3s ease-out}.xp-open{max-height:3000px;transition:max-height .5s ease-in}
|
|
16264
|
+
.card-sel{outline:2px solid rgba(59,130,246,.4);outline-offset:1px}
|
|
16265
|
+
.md pre{background:#1a1f2e;border:1px solid #1e2433;border-radius:6px;padding:8px 12px;overflow-x:auto;margin:6px 0;font-size:12px;line-height:1.5}
|
|
16266
|
+
.md code{background:#1a1f2e;padding:1px 5px;border-radius:3px;font-size:12px}.md pre code{background:none;padding:0}
|
|
16267
|
+
.md strong{color:#e2e8f0;font-weight:600}.md em{color:#c1c9d9;font-style:italic}
|
|
16268
|
+
.md ul,.md ol{padding-left:18px;margin:4px 0}.md li{margin:2px 0}
|
|
16269
|
+
.md h1,.md h2,.md h3{color:#e2e8f0;font-weight:600;margin:8px 0 4px}.md h1{font-size:15px}.md h2{font-size:14px}.md h3{font-size:13px}
|
|
16270
|
+
.md a{color:#60a5fa;text-decoration:underline}.md p{margin:4px 0}
|
|
16271
|
+
#sco{display:none;position:fixed;inset:0;background:rgba(12,14,20,.85);backdrop-filter:blur(8px);z-index:100;align-items:center;justify-content:center}
|
|
16272
|
+
#sco.show{display:flex}
|
|
16273
|
+
#drawer{position:fixed;top:0;right:0;bottom:0;width:min(480px,85vw);background:#141822;border-left:1px solid #1e2433;z-index:90;transform:translateX(100%);transition:transform .25s ease-out;overflow-y:auto}
|
|
16274
|
+
#drawer.open{transform:translateX(0)}
|
|
16275
|
+
#drawer-bg{position:fixed;inset:0;background:rgba(12,14,20,.5);z-index:89;display:none}
|
|
16276
|
+
#drawer-bg.open{display:block}
|
|
16277
|
+
</style>
|
|
16278
|
+
</head>
|
|
16279
|
+
<body class="bg-base-950 text-txt-100 min-h-screen antialiased font-sans">
|
|
16280
|
+
<header class="fixed top-0 inset-x-0 h-11 bg-base-950/95 backdrop-blur border-b border-base-800 flex items-center justify-between px-4 z-50">
|
|
16281
|
+
<div class="flex items-center gap-3">
|
|
16282
|
+
<span class="font-mono font-semibold text-[13px] tracking-[.08em] text-txt-200">ensemble</span>
|
|
16283
|
+
<span class="text-base-700">|</span>
|
|
16284
|
+
<select id="sel" class="bg-base-900 border border-base-700 rounded-md px-2 py-[3px] text-[11px] text-txt-200 font-mono outline-none cursor-pointer hover:border-base-600 transition-colors"></select>
|
|
16285
|
+
</div>
|
|
16286
|
+
<div class="flex items-center gap-4">
|
|
16287
|
+
<div id="hring" class="w-6 h-6 rounded-full" title="Team health"></div>
|
|
16288
|
+
<div class="flex items-center gap-2">
|
|
16289
|
+
<span id="clk" class="text-[11px] text-txt-400 font-mono"></span>
|
|
16290
|
+
<span class="text-base-700">\xB7</span>
|
|
16291
|
+
<span id="cd" class="w-[7px] h-[7px] rounded-full bg-emerald-500 pulse"></span>
|
|
16292
|
+
<span id="ct" class="text-[11px] text-txt-400 font-mono">...</span>
|
|
16293
|
+
</div>
|
|
16294
|
+
</div>
|
|
16295
|
+
</header>
|
|
16296
|
+
<div id="sum" class="fixed top-11 inset-x-0 h-8 bg-base-900/80 backdrop-blur border-b border-base-800/50 flex items-center px-4 gap-4 text-[11px] text-txt-300 z-40"></div>
|
|
16297
|
+
<main class="pt-[76px] px-4 pb-16">
|
|
16298
|
+
<div id="empty" class="hidden flex-col items-center justify-center h-[70vh] gap-3">
|
|
16299
|
+
<div class="w-12 h-12 rounded-full border-2 border-base-700 flex items-center justify-center mb-2"><svg class="w-5 h-5 text-txt-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 4.5v15m7.5-7.5h-15"/></svg></div>
|
|
16300
|
+
<div class="text-txt-400 text-sm">Waiting for a team</div>
|
|
16301
|
+
<div class="text-txt-500 text-[11px]">Run <code class="px-1.5 py-0.5 bg-base-900 rounded text-txt-300 font-mono text-[11px]">team_create</code> in OpenCode to get started</div>
|
|
16302
|
+
</div>
|
|
16303
|
+
<div id="content" class="hidden">
|
|
16304
|
+
<div id="agents" class="grid gap-2 mb-4" style="grid-template-columns:repeat(auto-fill,minmax(300px,1fr))"></div>
|
|
16305
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
16306
|
+
<div id="tasks" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div>
|
|
16307
|
+
<div id="activity" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div>
|
|
16308
|
+
</div>
|
|
16309
|
+
</div>
|
|
16310
|
+
</main>
|
|
16311
|
+
<div id="tl" class="fixed bottom-0 inset-x-0 h-10 bg-base-900/90 backdrop-blur border-t border-base-800 px-4 flex items-center z-40 overflow-x-auto scroll hidden"></div>
|
|
16312
|
+
<div id="sco" onclick="this.classList.remove('show')">
|
|
16313
|
+
<div class="bg-base-900 border border-base-800 rounded-lg p-6 max-w-sm" onclick="event.stopPropagation()">
|
|
16314
|
+
<div class="text-txt-200 font-semibold text-sm mb-4">Keyboard Shortcuts</div>
|
|
16315
|
+
<div class="grid grid-cols-2 gap-y-2 gap-x-6 text-[12px]">
|
|
16316
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">j</kbd> <span class="text-txt-400">Next agent</span></div>
|
|
16317
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">k</kbd> <span class="text-txt-400">Prev agent</span></div>
|
|
16318
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">Enter</kbd> <span class="text-txt-400">Expand agent</span></div>
|
|
16319
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">Esc</kbd> <span class="text-txt-400">Collapse all</span></div>
|
|
16320
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">1-9</kbd> <span class="text-txt-400">Switch team</span></div>
|
|
16321
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">?</kbd> <span class="text-txt-400">This help</span></div>
|
|
16322
|
+
</div>
|
|
16323
|
+
</div>
|
|
16324
|
+
</div>
|
|
16325
|
+
<div id="drawer-bg" onclick="closeDrawer()"></div>
|
|
16326
|
+
<div id="drawer" class="scroll p-4"></div>`;
|
|
16327
|
+
|
|
16328
|
+
// src/dashboard-js-part1.ts
|
|
16329
|
+
var DASHBOARD_JS_PART1 = `
|
|
16330
|
+
let S=null,selId=null,fails=0,pollT=Date.now(),prevMC=0,selCard=-1;
|
|
16331
|
+
const expCards=new Set(),expMsgs=new Set();
|
|
16332
|
+
const E=s=>s?String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'):'';
|
|
16333
|
+
const D=ms=>{const s=Math.floor(Math.abs(ms)/1000);return s<60?s+'s':s<3600?Math.floor(s/60)+'m':Math.floor(s/3600)+'h'};
|
|
16334
|
+
const T=e=>new Date(e).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
16335
|
+
function relT(ep){const ms=Date.now()-ep;if(ms<60000)return Math.floor(ms/1000)+'s ago';if(ms<3600000)return Math.floor(ms/60000)+'m ago';if(ms<86400000)return Math.floor(ms/3600000)+'h ago';return T(ep)}
|
|
16336
|
+
|
|
16337
|
+
// Chip: small readable badge with background tint
|
|
16338
|
+
function chip(text,color){
|
|
16339
|
+
var colors={
|
|
16340
|
+
blue:'bg-blue-500/15 text-blue-400 border-blue-500/20',
|
|
16341
|
+
green:'bg-emerald-500/15 text-emerald-400 border-emerald-500/20',
|
|
16342
|
+
amber:'bg-amber-500/15 text-amber-400 border-amber-500/20',
|
|
16343
|
+
red:'bg-red-500/15 text-red-400 border-red-500/20',
|
|
16344
|
+
gray:'bg-base-700/40 text-txt-300 border-base-700/30',
|
|
16345
|
+
muted:'bg-base-800/60 text-txt-400 border-base-700/20',
|
|
16346
|
+
};
|
|
16347
|
+
var c=colors[color]||colors.muted;
|
|
16348
|
+
return '<span class="inline-flex items-center px-1.5 py-[1px] rounded text-[10px] font-medium border '+c+'">'+text+'</span>';
|
|
16349
|
+
}
|
|
16350
|
+
|
|
16351
|
+
function md(s){
|
|
16352
|
+
let h=E(s);
|
|
16353
|
+
h=h.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,function(_,l,c){return '<pre><code>'+c.trim()+'</code></pre>'});
|
|
16354
|
+
h=h.replace(/\`([^\`]+)\`/g,'<code>$1</code>');
|
|
16355
|
+
h=h.replace(/^### (.+)$/gm,'<h3>$1</h3>');
|
|
16356
|
+
h=h.replace(/^## (.+)$/gm,'<h2>$1</h2>');
|
|
16357
|
+
h=h.replace(/^# (.+)$/gm,'<h1>$1</h1>');
|
|
16358
|
+
h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
|
|
16359
|
+
h=h.replace(/\\*(.+?)\\*/g,'<em>$1</em>');
|
|
16360
|
+
h=h.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
16361
|
+
h=h.replace(/^- (.+)$/gm,'<li>$1</li>');
|
|
16362
|
+
h=h.replace(/(<li>.*<\\/li>\\n?)+/g,'<ul>$&</ul>');
|
|
16363
|
+
h=h.replace(/^\\d+\\. (.+)$/gm,'<li>$1</li>');
|
|
16364
|
+
h=h.replace(/\\n\\n/g,'</p><p>');
|
|
16365
|
+
h=h.replace(/\\n/g,'<br>');
|
|
16366
|
+
return '<p>'+h+'</p>';
|
|
16367
|
+
}
|
|
16368
|
+
|
|
16369
|
+
function saveD(el){const s={};el.querySelectorAll('details').forEach((d,i)=>{s[i]=d.open});return s}
|
|
16370
|
+
function restD(el,s){if(!s)return;el.querySelectorAll('details').forEach((d,i)=>{if(i in s)d.open=s[i]})}
|
|
16371
|
+
function saveSc(el){const s=el.querySelector('.scroll');return s?s.scrollTop:0}
|
|
16372
|
+
function restSc(el,p){const s=el.querySelector('.scroll');if(s)s.scrollTop=p}
|
|
16373
|
+
function patch(el,h){if(el.innerHTML!==h)el.innerHTML=h}
|
|
16374
|
+
|
|
16375
|
+
const ST={
|
|
16376
|
+
busy:{c:'border-blue-500 bg-blue-500/[0.04]',d:'bg-blue-500',t:'text-blue-400',l:'working',dim:false},
|
|
16377
|
+
ready:{c:'border-base-700 bg-base-950/50',d:'bg-txt-400',t:'text-txt-400',l:'idle',dim:true},
|
|
16378
|
+
shutdown_requested:{c:'border-amber-500/50 bg-amber-500/[0.04]',d:'bg-amber-500',t:'text-amber-400',l:'stopping',dim:false},
|
|
16379
|
+
shutdown:{c:'border-base-800 bg-base-950/30',d:'bg-base-700',t:'text-txt-500',l:'done',dim:true},
|
|
16380
|
+
error:{c:'border-red-500/50 bg-red-500/[0.04]',d:'bg-red-500',t:'text-red-400',l:'error',dim:false},
|
|
16381
|
+
};
|
|
16382
|
+
const si=s=>ST[s]||ST.ready;
|
|
16383
|
+
const PR={high:'text-red-400 bg-red-500/10 border border-red-500/20',medium:'text-amber-400 bg-amber-500/10 border border-amber-500/20',low:'text-txt-400 bg-base-800 border border-base-700'};
|
|
16384
|
+
|
|
16385
|
+
function parseR(c){const m=c.match(/<task-result>([\\s\\S]*?)<\\/task-result>/);if(!m)return null;const i=m[1],s=(i.match(/<status>([\\s\\S]*?)<\\/status>/)||[])[1]?.trim(),u=(i.match(/<summary>([\\s\\S]*?)<\\/summary>/)||[])[1]?.trim(),d=(i.match(/<details>([\\s\\S]*?)<\\/details>/)||[])[1]?.trim();return s&&u?{status:s,summary:u,details:d||''}:null}
|
|
16386
|
+
|
|
16387
|
+
function allTeams(){
|
|
16388
|
+
if(!S?.teams)return{active:[],archived:[]};
|
|
16389
|
+
const active=[...S.teams.filter(t=>t.status==='active')].sort((a,b)=>b.timeUpdated-a.timeUpdated);
|
|
16390
|
+
const archived=[...S.teams.filter(t=>t.status!=='active')].sort((a,b)=>b.timeUpdated-a.timeUpdated);
|
|
16391
|
+
return{active,archived};
|
|
16392
|
+
}
|
|
16393
|
+
function cur(){const{active,archived}=allTeams(),all=[...active,...archived];if(!all.length)return null;if(selId){const t=all.find(t=>t.id===selId);if(t)return t}return active[0]||all[0]}
|
|
16394
|
+
|
|
16395
|
+
function deriveHealth(t){
|
|
16396
|
+
const mm=t.members||[];if(!mm.length)return{w:0,i:0,e:0,d:0,total:0};
|
|
16397
|
+
return{w:mm.filter(m=>m.status==='busy').length,i:mm.filter(m=>m.status==='ready').length,e:mm.filter(m=>m.status==='error').length,d:mm.filter(m=>m.status==='shutdown'||m.status==='shutdown_requested').length,total:mm.length};
|
|
16398
|
+
}
|
|
16399
|
+
|
|
16400
|
+
function deriveSparkline(name,msgs){
|
|
16401
|
+
const mine=msgs.filter(m=>m.fromName===name).map(m=>m.timeCreated).sort();
|
|
16402
|
+
if(mine.length<2)return '';
|
|
16403
|
+
const min=mine[0],max=mine[mine.length-1],range=max-min||1;
|
|
16404
|
+
const buckets=new Array(12).fill(0);
|
|
16405
|
+
mine.forEach(t=>{const idx=Math.min(11,Math.floor((t-min)/range*12));buckets[idx]++});
|
|
16406
|
+
const mx=Math.max(...buckets)||1;
|
|
16407
|
+
const bars=buckets.map((v,i)=>{const h=Math.max(1,Math.round(v/mx*14));return '<rect x="'+(i*5)+'" y="'+(14-h)+'" width="3.5" height="'+h+'" rx="0.5" fill="currentColor" opacity="'+(0.3+v/mx*0.7)+'"/>'}).join('');
|
|
16408
|
+
return '<svg class="inline-block text-blue-500/60" width="60" height="14" viewBox="0 0 60 14">'+bars+'</svg>';
|
|
16409
|
+
}
|
|
16410
|
+
|
|
16411
|
+
function deriveTimeline(t){
|
|
16412
|
+
const ev=[];
|
|
16413
|
+
(t.members||[]).forEach(m=>{ev.push({t:m.timeCreated,type:'spawn',label:E(m.name)+' spawned',c:'bg-blue-400'});if(m.status==='shutdown')ev.push({t:m.timeUpdated,type:'off',label:E(m.name)+' shut down',c:'bg-txt-500'});if(m.status==='error')ev.push({t:m.timeUpdated,type:'err',label:E(m.name)+' error',c:'bg-red-500'})});
|
|
16414
|
+
(t.messages||[]).forEach(m=>{const p=parseR(m.content);ev.push({t:m.timeCreated,type:'msg',label:E(m.fromName)+' \\u2192 '+(E(m.toName)||'all'),c:p?'bg-emerald-500':'bg-blue-400'})});
|
|
16415
|
+
(t.tasks||[]).filter(x=>x.status==='completed').forEach(x=>{ev.push({t:x.timeUpdated,type:'done',label:'Task done',c:'bg-emerald-500'})});
|
|
16416
|
+
return ev.sort((a,b)=>a.t-b.t).slice(-50);
|
|
16417
|
+
}
|
|
16418
|
+
|
|
16419
|
+
function deriveThreads(msgs){
|
|
16420
|
+
const threads={};
|
|
16421
|
+
msgs.forEach(m=>{const k=m.fromName;if(!threads[k])threads[k]={from:m.fromName,msgs:[]};threads[k].msgs.push(m)});
|
|
16422
|
+
return Object.values(threads).sort((a,b)=>b.msgs[0].timeCreated-a.msgs[0].timeCreated);
|
|
16423
|
+
}
|
|
16424
|
+
`;
|
|
16425
|
+
|
|
16426
|
+
// src/dashboard-js-part2.ts
|
|
16427
|
+
var DASHBOARD_JS_PART2 = `
|
|
16428
|
+
function rSel(){
|
|
16429
|
+
const el=document.getElementById('sel'),{active,archived}=allTeams(),c=cur();
|
|
16430
|
+
if(!active.length&&!archived.length){el.innerHTML='<option>No teams</option>';return}
|
|
16431
|
+
let h='';
|
|
16432
|
+
if(active.length){h+='<optgroup label="Active">';h+=active.map(t=>'<option value="'+t.id+'">'+E(t.name)+' ('+relT(t.timeUpdated)+')</option>').join('');h+='</optgroup>'}
|
|
16433
|
+
if(archived.length){h+='<optgroup label="Archived">';h+=archived.slice(0,5).map(t=>'<option value="'+t.id+'">'+E(t.name)+'</option>').join('');if(archived.length>5)h+='<option disabled>+ '+(archived.length-5)+' more</option>';h+='</optgroup>'}
|
|
16434
|
+
if(el._lh!==h){el.innerHTML=h;el._lh=h}
|
|
16435
|
+
if(c)el.value=c.id;
|
|
16436
|
+
}
|
|
16437
|
+
|
|
16438
|
+
function rHealth(t){
|
|
16439
|
+
const el=document.getElementById('hring'),h=deriveHealth(t);
|
|
16440
|
+
if(!h.total){el.style.background='conic-gradient(#2a3144 0deg,#2a3144 360deg)';return}
|
|
16441
|
+
const total=h.total,segs=[];let deg=0;
|
|
16442
|
+
if(h.w){const d=h.w/total*360;segs.push('#3b82f6 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16443
|
+
if(h.i){const d=h.i/total*360;segs.push('#5e6a82 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16444
|
+
if(h.e){const d=h.e/total*360;segs.push('#ef4444 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16445
|
+
if(h.d){const d=h.d/total*360;segs.push('#2a3144 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16446
|
+
el.style.background='conic-gradient('+segs.join(',')+')';
|
|
16447
|
+
el.style.mask='radial-gradient(circle at center,transparent 7px,black 8px)';
|
|
16448
|
+
el.style.webkitMask=el.style.mask;
|
|
16449
|
+
}
|
|
16450
|
+
|
|
16451
|
+
function rClock(t){
|
|
16452
|
+
const el=document.getElementById('clk');
|
|
16453
|
+
const now=new Date();
|
|
16454
|
+
const hh=String(now.getHours()).padStart(2,'0'),mm=String(now.getMinutes()).padStart(2,'0'),ss=String(now.getSeconds()).padStart(2,'0');
|
|
16455
|
+
const uptime=t?D(Date.now()-t.timeCreated):'';
|
|
16456
|
+
el.textContent=hh+':'+mm+':'+ss+(uptime?' \\u00B7 '+uptime:'');
|
|
16457
|
+
}
|
|
16458
|
+
|
|
16459
|
+
function rSum(t){
|
|
16460
|
+
const el=document.getElementById('sum'),mm=t.members||[],tk=t.tasks||[],msgs=t.messages||[];
|
|
16461
|
+
const h=deriveHealth(t),tc=tk.filter(x=>x.status==='completed').length;
|
|
16462
|
+
patch(el,
|
|
16463
|
+
'<span class="w-[6px] h-[6px] rounded-full '+(t.status==='active'?'bg-emerald-500':'bg-txt-500')+'"></span>'+
|
|
16464
|
+
'<span class="font-mono font-semibold text-txt-200">'+E(t.name)+'</span>'+
|
|
16465
|
+
(mm.length?chip(mm.length+' agent'+(mm.length!==1?'s':''),'gray'):'')+
|
|
16466
|
+
(h.w?chip(h.w+' working','blue'):'')+
|
|
16467
|
+
(h.i?chip(h.i+' idle','muted'):'')+
|
|
16468
|
+
(h.d?chip(h.d+' done','muted'):'')+
|
|
16469
|
+
(h.e?chip(h.e+' error','red'):'')+
|
|
16470
|
+
(tk.length?chip(tc+'/'+tk.length+' tasks','green'):'')+
|
|
16471
|
+
(msgs.length?chip(msgs.length+' msgs','muted'):'')
|
|
16472
|
+
);
|
|
16473
|
+
}
|
|
16474
|
+
|
|
16475
|
+
function rAgents(t){
|
|
16476
|
+
const el=document.getElementById('agents'),mm=t.members||[];
|
|
16477
|
+
if(!mm.length){patch(el,'<div class="col-span-full text-center py-12"><div class="text-txt-400 text-sm mb-1">No agents yet</div><div class="text-txt-500 text-[11px]">Spawn teammates with <code class="px-1 py-0.5 bg-base-900 rounded font-mono text-[10px]">team_spawn</code></div></div>');return}
|
|
16478
|
+
const n=Date.now(),msgs=t.messages||[],sorted=[...mm].sort((a,b)=>{const o={busy:0,ready:1,shutdown_requested:2,error:3,shutdown:4};return(o[a.status]??5)-(o[b.status]??5)});
|
|
16479
|
+
const html=sorted.map((m,idx)=>{
|
|
16480
|
+
const s=si(m.status),task=(t.tasks||[]).find(x=>x.assignee===m.name&&x.status==='in_progress'),msg=msgs.find(x=>x.fromName===m.name);
|
|
16481
|
+
const d=D(n-m.timeUpdated),mi=msg?relT(msg.timeCreated):'\\u2014';
|
|
16482
|
+
const tt=task?.content,tr=tt&&tt.length>90?tt.slice(0,90)+'\\u2026':tt;
|
|
16483
|
+
const spark=deriveSparkline(m.name,msgs);
|
|
16484
|
+
const isSel=selCard===idx;
|
|
16485
|
+
return '<div class="rounded-lg border '+s.c+' p-3 transition-all duration-300 cursor-pointer hover:border-base-600'+(s.dim?' opacity-50 hover:opacity-70':'')+(isSel?' card-sel':'')+'" data-card="'+E(m.name)+'" onclick="openDrawer(\\''+E(m.name)+'\\')">'+
|
|
16486
|
+
'<div class="flex items-center gap-2">'+
|
|
16487
|
+
'<span class="w-[8px] h-[8px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+' shrink-0"></span>'+
|
|
16488
|
+
'<span class="font-mono font-semibold text-[14px] truncate">'+E(m.name)+'</span>'+
|
|
16489
|
+
'<span class="text-[10px] px-1.5 py-[1px] rounded '+s.t+' bg-base-800/80 shrink-0">'+s.l+'</span>'+
|
|
16490
|
+
spark+
|
|
16491
|
+
'<span class="text-[10px] text-txt-500 ml-auto shrink-0">'+E(m.agent)+'</span>'+
|
|
16492
|
+
'</div>'+
|
|
16493
|
+
(tr?'<div class="mt-2 text-[13px] text-txt-200 leading-snug truncate">'+E(tr)+'</div>':'')+
|
|
16494
|
+
'<div class="mt-2 flex items-center gap-1.5 flex-wrap">'+
|
|
16495
|
+
chip(d,'muted')+chip('msg '+mi,'muted')+
|
|
16496
|
+
(m.worktreeBranch?chip(E(m.worktreeBranch),'muted'):'')+
|
|
16497
|
+
'</div></div>';
|
|
16498
|
+
}).join('');
|
|
16499
|
+
patch(el,html);
|
|
16500
|
+
}
|
|
16501
|
+
|
|
16502
|
+
function openDrawer(name){
|
|
16503
|
+
var t=cur();if(!t)return;
|
|
16504
|
+
var m=(t.members||[]).find(x=>x.name===name);if(!m)return;
|
|
16505
|
+
var s=si(m.status),msgs=(t.messages||[]).filter(x=>x.fromName===name);
|
|
16506
|
+
var h='';
|
|
16507
|
+
// Header
|
|
16508
|
+
h+='<div class="flex items-center justify-between mb-4">';
|
|
16509
|
+
h+='<div class="flex items-center gap-2"><span class="w-[10px] h-[10px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+'"></span><span class="font-mono font-semibold text-[16px]">'+E(m.name)+'</span><span class="text-[11px] px-2 py-[2px] rounded '+s.t+' bg-base-800/80">'+s.l+'</span></div>';
|
|
16510
|
+
h+='<button onclick="closeDrawer()" class="text-txt-500 hover:text-txt-200 transition-colors p-1"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg></button>';
|
|
16511
|
+
h+='</div>';
|
|
16512
|
+
// Meta chips
|
|
16513
|
+
var meta=[];
|
|
16514
|
+
meta.push(chip(E(m.agent),'gray'));
|
|
16515
|
+
if(m.model)meta.push(chip(E(m.model),'gray'));
|
|
16516
|
+
meta.push(chip(E(m.executionStatus||m.status),m.status==='busy'?'blue':m.status==='error'?'red':'muted'));
|
|
16517
|
+
if(m.planApproval&&m.planApproval!=='none')meta.push(chip(E(m.planApproval),m.planApproval==='approved'?'green':m.planApproval==='rejected'?'red':'amber'));
|
|
16518
|
+
meta.push(chip('spawned '+relT(m.timeCreated),'muted'));
|
|
16519
|
+
if(m.worktreeBranch)meta.push(chip(E(m.worktreeBranch),'muted'));
|
|
16520
|
+
h+='<div class="flex flex-wrap gap-1.5 mb-4 pb-4 border-b border-base-800/50">'+meta.join('')+'</div>';
|
|
16521
|
+
// Prompt
|
|
16522
|
+
if(m.prompt){
|
|
16523
|
+
h+='<div class="mb-4"><div class="text-txt-400 text-[10px] uppercase tracking-wider mb-2">Original Prompt</div>';
|
|
16524
|
+
h+='<div class="text-[13px] text-txt-200 md bg-base-800/20 rounded-lg p-3 border border-base-800/30">'+md(m.prompt)+'</div></div>';
|
|
16525
|
+
}
|
|
16526
|
+
// Chat log
|
|
16527
|
+
h+='<div class="text-txt-400 text-[10px] uppercase tracking-wider mb-3">Conversation</div>';
|
|
16528
|
+
if(!msgs.length){h+='<div class="text-txt-500 text-[12px]">No messages yet</div>'}
|
|
16529
|
+
else{
|
|
16530
|
+
// Get all messages involving this agent (sent or received)
|
|
16531
|
+
var allMsgs=(t.messages||[]).filter(function(x){return x.fromName===name||x.toName===name});
|
|
16532
|
+
allMsgs.sort(function(a,b){return a.timeCreated-b.timeCreated});
|
|
16533
|
+
allMsgs.forEach(function(am){
|
|
16534
|
+
var isAgent=am.fromName===name;
|
|
16535
|
+
var p=parseR(am.content);
|
|
16536
|
+
var deliv=am.delivered?(am.read?'\\u2713\\u2713':'\\u2713'):'';
|
|
16537
|
+
var align=isAgent?'mr-8':'ml-8';
|
|
16538
|
+
var bubbleBg=isAgent?'bg-blue-500/[0.07] border-blue-500/20':'bg-base-800/40 border-base-700/30';
|
|
16539
|
+
var sender=isAgent?E(am.fromName):'lead';
|
|
16540
|
+
h+='<div class="mb-2 '+align+'">';
|
|
16541
|
+
h+='<div class="rounded-xl border '+bubbleBg+' p-3">';
|
|
16542
|
+
h+='<div class="flex items-center gap-2 mb-1.5">';
|
|
16543
|
+
h+='<span class="text-[10px] font-medium '+(isAgent?'text-blue-400':'text-txt-300')+'">'+sender+'</span>';
|
|
16544
|
+
h+=chip(relT(am.timeCreated),'muted');
|
|
16545
|
+
if(am.toName)h+=chip('\\u2192 '+E(am.toName),'muted');
|
|
16546
|
+
if(deliv)h+='<span class="text-txt-500 text-[10px] ml-auto">'+deliv+'</span>';
|
|
16547
|
+
h+='</div>';
|
|
16548
|
+
if(p){
|
|
16549
|
+
h+='<div class="mb-1">'+chip(E(p.status),p.status==='completed'?'green':'red')+' <span class="text-[13px] text-txt-200 font-medium">'+E(p.summary)+'</span></div>';
|
|
16550
|
+
if(p.details)h+='<div class="text-[12px] text-txt-300 md mt-2">'+md(p.details)+'</div>';
|
|
16551
|
+
}else{
|
|
16552
|
+
h+='<div class="text-[12px] text-txt-300 md">'+md(am.content)+'</div>';
|
|
16553
|
+
}
|
|
16554
|
+
h+='</div></div>';
|
|
16555
|
+
});
|
|
16556
|
+
}
|
|
16557
|
+
document.getElementById('drawer').innerHTML=h;
|
|
16558
|
+
document.getElementById('drawer').classList.add('open');
|
|
16559
|
+
document.getElementById('drawer-bg').classList.add('open');
|
|
16560
|
+
}
|
|
16561
|
+
|
|
16562
|
+
function closeDrawer(){
|
|
16563
|
+
document.getElementById('drawer').classList.remove('open');
|
|
16564
|
+
document.getElementById('drawer-bg').classList.remove('open');
|
|
16565
|
+
}
|
|
16566
|
+
|
|
16567
|
+
function rTasks(t){
|
|
16568
|
+
const el=document.getElementById('tasks'),tk=t.tasks||[];
|
|
16569
|
+
if(!tk.length){patch(el,'<div class="text-txt-500 text-sm py-6 text-center">No tasks yet</div>');return}
|
|
16570
|
+
const c=tk.filter(x=>x.status==='completed').length,pct=Math.max(Math.round(c/tk.length*100),tk.length>0?2:0);
|
|
16571
|
+
const allDone=c===tk.length&&tk.length>0;
|
|
16572
|
+
const g={in_progress:[],pending:[],completed:[],blocked:[],cancelled:[]};for(const x of tk)(g[x.status]||(g[x.status]=[])).push(x);
|
|
16573
|
+
const ds=saveD(el);
|
|
16574
|
+
const taskMap={};tk.forEach(x=>{taskMap[x.id]=x});
|
|
16575
|
+
function grp(label,items,dc,defOpen){
|
|
16576
|
+
if(!items.length)return'';
|
|
16577
|
+
const rows=items.map(x=>{
|
|
16578
|
+
const ic=x.status==='completed'?'\\u2713':x.status==='in_progress'?'\\u25CF':'\\u25CB';
|
|
16579
|
+
const icl=x.status==='completed'?'text-emerald-500':x.status==='in_progress'?'text-blue-400':'text-txt-500';
|
|
16580
|
+
const p=PR[x.priority]||PR.low,cn=x.content.length>55?x.content.slice(0,55)+'\\u2026':x.content;
|
|
16581
|
+
let dep='';
|
|
16582
|
+
if(x.dependsOn){const parent=taskMap[x.dependsOn];dep='<div class="text-[10px] text-txt-500 mt-0.5">\\u2514\\u2192 '+(parent?E(parent.content.slice(0,30)):E(x.dependsOn))+'</div>'}
|
|
16583
|
+
return '<div class="flex items-start gap-2 py-1.5 px-1 rounded hover:bg-base-800/40 transition-colors">'+
|
|
16584
|
+
'<span class="'+icl+' text-[11px] mt-[2px] shrink-0">'+ic+'</span>'+
|
|
16585
|
+
'<div class="flex-1 min-w-0"><div class="text-[13px] text-txt-200 truncate">'+E(cn)+'</div>'+
|
|
16586
|
+
'<div class="flex gap-2 mt-1 text-[10px]"><span class="rounded px-1.5 py-[0px] '+p+'">'+E(x.priority)+'</span>'+
|
|
16587
|
+
'<span class="text-txt-500">'+(E(x.assignee)||'unassigned')+'</span></div>'+dep+'</div></div>';
|
|
16588
|
+
}).join('');
|
|
16589
|
+
return '<details '+(defOpen?'open':'')+'><summary class="flex items-center gap-2 text-[11px] text-txt-300 cursor-pointer py-1.5 hover:text-txt-200 transition-colors select-none">'+
|
|
16590
|
+
'<svg class="w-3 h-3 transition-transform [details[open]>summary>&]:rotate-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>'+
|
|
16591
|
+
'<span class="w-[6px] h-[6px] rounded-full '+dc+'"></span>'+label+' <span class="text-txt-500">('+items.length+')</span></summary>'+
|
|
16592
|
+
'<div class="ml-5 mt-1">'+rows+'</div></details>';
|
|
16593
|
+
}
|
|
16594
|
+
el.innerHTML='<div class="flex items-center gap-3 mb-3">'+
|
|
16595
|
+
'<span class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider">Tasks</span>'+
|
|
16596
|
+
'<div class="flex-1 bg-base-800 rounded-full h-[5px]"><div class="'+(allDone?'shimmer':'bg-emerald-500')+' h-[5px] rounded-full transition-all duration-500" style="width:'+pct+'%"></div></div>'+
|
|
16597
|
+
'<span class="text-[11px] text-txt-400 font-mono">'+c+'/'+tk.length+'</span>'+
|
|
16598
|
+
(allDone?'<span class="text-emerald-400 text-[10px] font-medium">All complete</span>':'')+
|
|
16599
|
+
'</div>'+
|
|
16600
|
+
grp('In Progress',g.in_progress,'bg-blue-500',true)+grp('Pending',g.pending,'bg-txt-500',true)+grp('Blocked',g.blocked||[],'bg-amber-500',true)+grp('Completed',g.completed,'bg-emerald-500',false);
|
|
16601
|
+
restD(el,ds);
|
|
16602
|
+
}
|
|
16603
|
+
|
|
16604
|
+
function rActivity(t){
|
|
16605
|
+
const el=document.getElementById('activity'),msgs=t.messages||[];
|
|
16606
|
+
if(!msgs.length){patch(el,'<div class="text-txt-500 text-sm py-6 text-center">Waiting for agent messages...</div>');return}
|
|
16607
|
+
const sp=saveSc(el);
|
|
16608
|
+
const newCount=msgs.length,hasNew=newCount>prevMC&&prevMC>0;
|
|
16609
|
+
let html='<div class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider mb-3">Activity '+chip(msgs.length+' msgs','muted')+'</div>';
|
|
16610
|
+
html+='<div class="max-h-[50vh] overflow-y-auto scroll space-y-2">';
|
|
16611
|
+
msgs.slice(0,30).forEach(function(m,mi){
|
|
16612
|
+
const isNew=hasNew&&mi<(newCount-prevMC);
|
|
16613
|
+
const isExp=expMsgs.has(m.id);
|
|
16614
|
+
const p=parseR(m.content);
|
|
16615
|
+
const deliv=m.delivered?(m.read?'\\u2713\\u2713':'\\u2713'):'';
|
|
16616
|
+
const isFromAgent=m.fromName!=='lead'&&m.fromName!=='system';
|
|
16617
|
+
const isPeer=isFromAgent&&m.toName&&m.toName!=='lead'&&m.toName!=='all';
|
|
16618
|
+
const align=isFromAgent?'mr-6':'ml-6';
|
|
16619
|
+
const bubbleBg=isPeer?'bg-violet-500/[0.06] border-violet-500/15':isFromAgent?'bg-blue-500/[0.06] border-blue-500/15':'bg-base-800/40 border-base-700/30';
|
|
16620
|
+
const initial=m.fromName.charAt(0).toUpperCase();
|
|
16621
|
+
const avatarColor=m.fromName==='system'?'bg-amber-500/20 text-amber-400':isPeer?'bg-violet-500/20 text-violet-400':isFromAgent?'bg-blue-500/20 text-blue-400':'bg-emerald-500/20 text-emerald-400';
|
|
16622
|
+
html+='<div class="'+align+(isNew?' hl':'')+' cursor-pointer" data-msg="'+E(m.id)+'" onclick="toggleMsg(\\''+E(m.id)+'\\')">';
|
|
16623
|
+
html+='<div class="flex items-start gap-2">';
|
|
16624
|
+
html+='<span class="w-5 h-5 rounded-full '+avatarColor+' flex items-center justify-center text-[9px] font-semibold shrink-0 mt-0.5">'+E(initial)+'</span>';
|
|
16625
|
+
html+='<div class="flex-1 min-w-0 rounded-xl border '+bubbleBg+' px-3 py-2">';
|
|
16626
|
+
html+='<div class="flex items-center gap-1.5 mb-1">';
|
|
16627
|
+
html+='<span class="text-[11px] font-medium '+(isPeer?'text-violet-400':isFromAgent?'text-blue-400':'text-txt-300')+'">'+E(m.fromName)+'</span>';
|
|
16628
|
+
if(m.toName)html+=chip('\\u2192 '+E(m.toName),'muted');
|
|
16629
|
+
html+=chip(relT(m.timeCreated),'muted');
|
|
16630
|
+
if(deliv)html+='<span class="text-txt-500 text-[10px] ml-auto">'+deliv+'</span>';
|
|
16631
|
+
html+='</div>';
|
|
16632
|
+
if(p){
|
|
16633
|
+
html+='<div>'+chip(E(p.status),p.status==='completed'?'green':'red')+' <span class="text-[13px] text-txt-200">'+E(p.summary)+'</span></div>';
|
|
16634
|
+
if(isExp&&p.details)html+='<div class="mt-2 text-[12px] text-txt-300 md">'+md(p.details)+'</div>';
|
|
16635
|
+
if(!isExp&&p.details)html+='<div class="mt-1 text-[10px] text-txt-500">Click to expand details</div>';
|
|
16636
|
+
}else{
|
|
16637
|
+
if(isExp){html+='<div class="text-[12px] text-txt-300 md">'+md(m.content)+'</div>'}
|
|
16638
|
+
else{const preview=m.content.length>120?m.content.slice(0,120)+'\\u2026':m.content;html+='<div class="text-[13px] text-txt-300 truncate">'+E(preview)+'</div>'}
|
|
16639
|
+
}
|
|
16640
|
+
html+='</div></div></div>';
|
|
16641
|
+
});
|
|
16642
|
+
if(msgs.length>30)html+='<div class="text-center text-[10px] text-txt-500 py-2">+'+(msgs.length-30)+' older messages</div>';
|
|
16643
|
+
html+='</div>';
|
|
16644
|
+
prevMC=newCount;
|
|
16645
|
+
el.innerHTML=html;
|
|
16646
|
+
restSc(el,sp);
|
|
16647
|
+
}
|
|
16648
|
+
|
|
16649
|
+
function rTimeline(t){
|
|
16650
|
+
const el=document.getElementById('tl'),evs=deriveTimeline(t);
|
|
16651
|
+
if(!evs.length){el.classList.add('hidden');return}
|
|
16652
|
+
el.classList.remove('hidden');
|
|
16653
|
+
const html=evs.map(ev=>{
|
|
16654
|
+
return '<div class="flex flex-col items-center mx-1 shrink-0 group" title="'+E(ev.label)+' \\u00B7 '+relT(ev.t)+'"><div class="w-[6px] h-[6px] rounded-full '+ev.c+' group-hover:scale-150 transition-transform"></div><div class="text-[8px] text-txt-500 mt-0.5 hidden group-hover:block whitespace-nowrap">'+E(ev.label)+'</div></div>';
|
|
16655
|
+
}).join('<div class="w-3 h-px bg-base-700 shrink-0 self-center"></div>');
|
|
16656
|
+
patch(el,'<span class="text-[9px] text-txt-500 uppercase tracking-wider mr-3 shrink-0">Timeline</span>'+html);
|
|
16657
|
+
}
|
|
16658
|
+
`;
|
|
16659
|
+
|
|
16660
|
+
// src/dashboard-js-part3.ts
|
|
16661
|
+
var DASHBOARD_JS_PART3 = `
|
|
16662
|
+
function toggleMsg(id){if(expMsgs.has(id))expMsgs.delete(id);else expMsgs.add(id);render()}
|
|
16663
|
+
|
|
16664
|
+
function render(){
|
|
16665
|
+
rSel();const t=cur();
|
|
16666
|
+
const empty=document.getElementById('empty'),content=document.getElementById('content');
|
|
16667
|
+
if(!t){empty.classList.remove('hidden');empty.classList.add('flex');content.classList.add('hidden');document.getElementById('tl').classList.add('hidden');return}
|
|
16668
|
+
empty.classList.add('hidden');empty.classList.remove('flex');content.classList.remove('hidden');
|
|
16669
|
+
rHealth(t);rSum(t);rAgents(t);rTasks(t);rActivity(t);rTimeline(t);
|
|
16670
|
+
}
|
|
16671
|
+
|
|
16672
|
+
function conn(ok){
|
|
16673
|
+
document.getElementById('cd').className='w-[7px] h-[7px] rounded-full '+(ok?'bg-emerald-500 pulse':'bg-red-500');
|
|
16674
|
+
document.getElementById('ct').textContent=ok?D(Date.now()-pollT)+' ago':'disconnected';
|
|
16675
|
+
}
|
|
16676
|
+
|
|
16677
|
+
async function poll(){try{S=await(await fetch('/api/state')).json();fails=0;pollT=Date.now();conn(true);render()}catch{if(++fails>=3)conn(false)}}
|
|
16678
|
+
|
|
16679
|
+
// Clock update every second
|
|
16680
|
+
setInterval(function(){var t=cur();if(t)rClock(t);if(fails<3)conn(true)},1000);
|
|
16681
|
+
|
|
16682
|
+
// Poll every 2.5s
|
|
16683
|
+
setInterval(poll,2500);
|
|
16684
|
+
|
|
16685
|
+
// Keyboard shortcuts
|
|
16686
|
+
document.addEventListener('keydown',function(e){
|
|
16687
|
+
if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT'||e.target.tagName==='TEXTAREA')return;
|
|
16688
|
+
var t=cur();if(!t)return;
|
|
16689
|
+
var mm=t.members||[];
|
|
16690
|
+
if(e.key==='?'){e.preventDefault();document.getElementById('sco').classList.toggle('show');return}
|
|
16691
|
+
if(e.key==='Escape'){closeDrawer();expMsgs.clear();selCard=-1;document.getElementById('sco').classList.remove('show');render();return}
|
|
16692
|
+
if(e.key==='j'&&mm.length){e.preventDefault();selCard=Math.min(selCard+1,mm.length-1);render();return}
|
|
16693
|
+
if(e.key==='k'&&mm.length){e.preventDefault();selCard=Math.max(selCard-1,0);render();return}
|
|
16694
|
+
if(e.key==='Enter'&&selCard>=0&&selCard<mm.length){e.preventDefault();openDrawer(mm[selCard].name);return}
|
|
16695
|
+
if(e.key>='1'&&e.key<='9'){
|
|
16696
|
+
var teams=allTeams(),all=[...teams.active,...teams.archived];
|
|
16697
|
+
var idx=parseInt(e.key)-1;
|
|
16698
|
+
if(idx<all.length){selId=all[idx].id;render()}
|
|
16699
|
+
}
|
|
16700
|
+
});
|
|
16701
|
+
|
|
16702
|
+
// Select handler
|
|
16703
|
+
document.getElementById('sel').addEventListener('change',function(){selId=this.value;render()});
|
|
16704
|
+
|
|
16705
|
+
// Initial poll
|
|
16706
|
+
poll();
|
|
16707
|
+
|
|
16708
|
+
console.log('%c Ensemble Mission Control','font-size:14px;font-weight:bold;color:#22c55e');
|
|
16709
|
+
`;
|
|
16710
|
+
|
|
16711
|
+
// src/dashboard.ts
|
|
16712
|
+
var DASHBOARD_HTML = DASHBOARD_HEAD + `
|
|
16713
|
+
<script>` + DASHBOARD_JS_PART1 + DASHBOARD_JS_PART2 + DASHBOARD_JS_PART3 + `</script>
|
|
16714
|
+
</body></html>`;
|
|
16715
|
+
function jsonResponse(data, status = 200) {
|
|
16716
|
+
return new Response(JSON.stringify(data), {
|
|
16717
|
+
status,
|
|
16718
|
+
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
|
|
16719
|
+
});
|
|
16720
|
+
}
|
|
16721
|
+
function buildState(db) {
|
|
16722
|
+
const teams = db.query("SELECT id, name, status, lead_agent, time_created, time_updated FROM team ORDER BY time_created DESC").all();
|
|
16723
|
+
const memberStmt = db.query("SELECT name, agent, status, execution_status, worktree_branch, prompt, model, plan_approval, time_created, time_updated FROM team_member WHERE team_id = ?");
|
|
16724
|
+
const taskStmt = db.query("SELECT id, content, status, priority, assignee, depends_on, time_created, time_updated FROM team_task WHERE team_id = ?");
|
|
16725
|
+
const msgStmt = db.query("SELECT id, from_name, to_name, content, delivered, read, time_created FROM team_message WHERE team_id = ? ORDER BY time_created DESC LIMIT 50");
|
|
16726
|
+
return {
|
|
16727
|
+
teams: teams.map((t) => ({
|
|
16728
|
+
id: t.id,
|
|
16729
|
+
name: t.name,
|
|
16730
|
+
status: t.status,
|
|
16731
|
+
leadAgent: t.lead_agent,
|
|
16732
|
+
timeCreated: t.time_created,
|
|
16733
|
+
timeUpdated: t.time_updated,
|
|
16734
|
+
members: memberStmt.all(t.id).map((m) => ({
|
|
16735
|
+
name: m.name,
|
|
16736
|
+
agent: m.agent,
|
|
16737
|
+
status: m.status,
|
|
16738
|
+
executionStatus: m.execution_status,
|
|
16739
|
+
worktreeBranch: m.worktree_branch,
|
|
16740
|
+
prompt: m.prompt,
|
|
16741
|
+
model: m.model,
|
|
16742
|
+
planApproval: m.plan_approval,
|
|
16743
|
+
timeCreated: m.time_created,
|
|
16744
|
+
timeUpdated: m.time_updated
|
|
16745
|
+
})),
|
|
16746
|
+
tasks: taskStmt.all(t.id).map((tk) => ({
|
|
16747
|
+
id: tk.id,
|
|
16748
|
+
content: tk.content,
|
|
16749
|
+
status: tk.status,
|
|
16750
|
+
priority: tk.priority,
|
|
16751
|
+
assignee: tk.assignee,
|
|
16752
|
+
dependsOn: tk.depends_on,
|
|
16753
|
+
timeCreated: tk.time_created,
|
|
16754
|
+
timeUpdated: tk.time_updated
|
|
16755
|
+
})),
|
|
16756
|
+
messages: msgStmt.all(t.id).map((msg) => ({
|
|
16757
|
+
id: msg.id,
|
|
16758
|
+
fromName: msg.from_name,
|
|
16759
|
+
toName: msg.to_name,
|
|
16760
|
+
content: msg.content,
|
|
16761
|
+
delivered: msg.delivered === 1,
|
|
16762
|
+
read: msg.read === 1,
|
|
16763
|
+
timeCreated: msg.time_created
|
|
16764
|
+
}))
|
|
16765
|
+
}))
|
|
16766
|
+
};
|
|
16767
|
+
}
|
|
16768
|
+
async function startDashboard(db, port) {
|
|
16769
|
+
try {
|
|
16770
|
+
const server2 = Bun.serve({
|
|
16771
|
+
port,
|
|
16772
|
+
fetch(req) {
|
|
16773
|
+
const url2 = new URL(req.url);
|
|
16774
|
+
if (url2.pathname === "/api/health") {
|
|
16775
|
+
return jsonResponse({ ensemble: true, pid: process.pid });
|
|
16776
|
+
}
|
|
16777
|
+
if (url2.pathname === "/api/state") {
|
|
16778
|
+
return jsonResponse(buildState(db));
|
|
16779
|
+
}
|
|
16780
|
+
if (url2.pathname === "/") {
|
|
16781
|
+
return new Response(DASHBOARD_HTML, {
|
|
16782
|
+
headers: { "Content-Type": "text/html" }
|
|
16783
|
+
});
|
|
16784
|
+
}
|
|
16785
|
+
return new Response("Not Found", { status: 404 });
|
|
16786
|
+
}
|
|
16787
|
+
});
|
|
16788
|
+
log(`dashboard:started port=${port} url=http://localhost:${port}`);
|
|
16789
|
+
return server2;
|
|
16790
|
+
} catch (err) {
|
|
16791
|
+
if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE") {
|
|
16792
|
+
try {
|
|
16793
|
+
const res = await fetch(`http://localhost:${port}/api/health`);
|
|
16794
|
+
const data = await res.json();
|
|
16795
|
+
if (data.ensemble && data.pid) {
|
|
16796
|
+
let alive = false;
|
|
16797
|
+
try {
|
|
16798
|
+
process.kill(data.pid, 0);
|
|
16799
|
+
alive = true;
|
|
16800
|
+
} catch {}
|
|
16801
|
+
if (alive && data.pid !== process.pid) {
|
|
16802
|
+
log(`dashboard:already-running port=${port} pid=${data.pid}`);
|
|
16803
|
+
return null;
|
|
16804
|
+
}
|
|
16805
|
+
log(`dashboard:stale-server port=${port} stale-pid=${data.pid} \u2014 run: kill -9 ${data.pid} || lsof -ti:${port} | xargs kill -9`);
|
|
16806
|
+
return null;
|
|
16807
|
+
}
|
|
16808
|
+
} catch {}
|
|
16809
|
+
log(`dashboard:port-in-use port=${port} (not an ensemble instance)`);
|
|
16810
|
+
return null;
|
|
16811
|
+
}
|
|
16812
|
+
log(`dashboard:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
16813
|
+
return null;
|
|
16174
16814
|
}
|
|
16175
16815
|
}
|
|
16176
16816
|
|
|
@@ -16370,6 +17010,9 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
16370
17010
|
} else {
|
|
16371
17011
|
context.push("", "Tools available to you:", "- team_message: send a message to the lead or another teammate", "- team_broadcast: send a message to all team members", "- team_tasks_list: view the shared team task board", "- team_tasks_add: add tasks to the shared board", "- team_tasks_complete: mark a task complete on the shared board", "- team_claim: claim a pending task from the shared board");
|
|
16372
17012
|
}
|
|
17013
|
+
if (otherMembers.length > 0) {
|
|
17014
|
+
context.push("", "Collaboration:", "- Check team_tasks_list to see what other teammates are working on.", "- If you need information another teammate has, message them directly via team_message.", "- If you discover something relevant to another teammate's task, share it with them.", "- Use team_broadcast for updates that affect the whole team.", "- Keep peer messages focused and actionable \u2014 coordinate, don't chat.");
|
|
17015
|
+
}
|
|
16373
17016
|
context.push("", "When you finish your task:");
|
|
16374
17017
|
if (!isReadOnly && worktreeBranch) {
|
|
16375
17018
|
context.push(`1. Commit your changes: git add -A && git commit -m "your summary"`);
|
|
@@ -16437,6 +17080,19 @@ async function executeTeamMessage(deps, args, sessionId) {
|
|
|
16437
17080
|
const teamInfo = requireTeamMember(deps, sessionId);
|
|
16438
17081
|
const senderName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName ?? "unknown";
|
|
16439
17082
|
const recipientSessionId = resolveRecipientSession(deps.db, deps.registry, teamInfo.teamId, args.to);
|
|
17083
|
+
if (!recipientSessionId && args.to !== "lead") {
|
|
17084
|
+
if (args.approve || args.reject) {
|
|
17085
|
+
throw new Error(`Cannot approve/reject plan for "${args.to}" \u2014 they haven't been spawned yet.`);
|
|
17086
|
+
}
|
|
17087
|
+
sendMessage(deps.db, {
|
|
17088
|
+
teamId: teamInfo.teamId,
|
|
17089
|
+
from: senderName,
|
|
17090
|
+
to: args.to,
|
|
17091
|
+
content: args.text
|
|
17092
|
+
});
|
|
17093
|
+
log(`team_message:queued from=${senderName} to=${args.to} (recipient not yet spawned)`);
|
|
17094
|
+
return `Message queued for ${args.to} \u2014 they haven't been spawned yet. It will be delivered when they join the team.`;
|
|
17095
|
+
}
|
|
16440
17096
|
if (!recipientSessionId)
|
|
16441
17097
|
throw new Error(`Recipient "${args.to}" not found in team "${teamInfo.teamName}"`);
|
|
16442
17098
|
let messageText = args.text;
|
|
@@ -16997,6 +17653,8 @@ class Watchdog {
|
|
|
16997
17653
|
stallMinSteps;
|
|
16998
17654
|
stallTokenThreshold;
|
|
16999
17655
|
cwd;
|
|
17656
|
+
peerMessageLimit;
|
|
17657
|
+
peerMessageWindowMs;
|
|
17000
17658
|
timer;
|
|
17001
17659
|
constructor(opts) {
|
|
17002
17660
|
this.db = opts.db;
|
|
@@ -17009,6 +17667,8 @@ class Watchdog {
|
|
|
17009
17667
|
this.stallMinSteps = opts.stallMinSteps ?? 3;
|
|
17010
17668
|
this.stallTokenThreshold = opts.stallTokenThreshold ?? 500;
|
|
17011
17669
|
this.cwd = opts.cwd;
|
|
17670
|
+
this.peerMessageLimit = opts.peerMessageLimit ?? 0;
|
|
17671
|
+
this.peerMessageWindowMs = opts.peerMessageWindowMs ?? 300000;
|
|
17012
17672
|
}
|
|
17013
17673
|
static STALE_THRESHOLD_MS = Number(process.env.STALE_WORKTREE_THRESHOLD_MS) || 300000;
|
|
17014
17674
|
async cleanupStaleWorktrees() {
|
|
@@ -17066,9 +17726,36 @@ class Watchdog {
|
|
|
17066
17726
|
} catch {}
|
|
17067
17727
|
}
|
|
17068
17728
|
}
|
|
17729
|
+
async checkChatty() {
|
|
17730
|
+
if (!this.progressTracker || this.peerMessageLimit === 0)
|
|
17731
|
+
return;
|
|
17732
|
+
const busy = this.db.query(`SELECT tm.team_id, tm.name, tm.session_id
|
|
17733
|
+
FROM team_member tm
|
|
17734
|
+
JOIN team t ON tm.team_id = t.id
|
|
17735
|
+
WHERE t.status = 'active' AND tm.status = 'busy'`).all();
|
|
17736
|
+
for (const member of busy) {
|
|
17737
|
+
if (this.progressTracker.isChattyReported(member.session_id))
|
|
17738
|
+
continue;
|
|
17739
|
+
if (!this.progressTracker.isChatty(member.session_id, this.peerMessageLimit, this.peerMessageWindowMs))
|
|
17740
|
+
continue;
|
|
17741
|
+
this.progressTracker.markChattyReported(member.session_id);
|
|
17742
|
+
this.client.session.promptAsync({
|
|
17743
|
+
sessionID: member.session_id,
|
|
17744
|
+
parts: [{ type: "text", text: "[System]: You've sent several messages to teammates. Focus on completing your task and send your results to the lead via team_message." }]
|
|
17745
|
+
}).catch(() => {});
|
|
17746
|
+
sendMessage(this.db, {
|
|
17747
|
+
teamId: member.team_id,
|
|
17748
|
+
from: "system",
|
|
17749
|
+
to: "lead",
|
|
17750
|
+
content: `Agent "${member.name}" is sending many peer messages and may be over-coordinating. Consider checking on them.`
|
|
17751
|
+
});
|
|
17752
|
+
log(`watchdog:chatty member=${member.name} limit=${this.peerMessageLimit}`);
|
|
17753
|
+
}
|
|
17754
|
+
}
|
|
17069
17755
|
async check() {
|
|
17070
17756
|
await this.cleanupStaleWorktrees();
|
|
17071
17757
|
await this.checkStalled();
|
|
17758
|
+
await this.checkChatty();
|
|
17072
17759
|
if (this.ttlMs === 0)
|
|
17073
17760
|
return;
|
|
17074
17761
|
const cutoff = Date.now() - this.ttlMs;
|
|
@@ -17155,6 +17842,11 @@ var plugin = async (input) => {
|
|
|
17155
17842
|
log(`init:recover-branches:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
17156
17843
|
});
|
|
17157
17844
|
log("init:recovery:done");
|
|
17845
|
+
if (config2.dashboardPort !== 0) {
|
|
17846
|
+
startDashboard(db, config2.dashboardPort).catch((err) => {
|
|
17847
|
+
log(`init:dashboard:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
17848
|
+
});
|
|
17849
|
+
}
|
|
17158
17850
|
} else {
|
|
17159
17851
|
log(`init:skip-recovery (worktree instance: ${input.directory})`);
|
|
17160
17852
|
}
|
|
@@ -17173,7 +17865,9 @@ var plugin = async (input) => {
|
|
|
17173
17865
|
stallThresholdMs: config2.stallThresholdMs,
|
|
17174
17866
|
stallMinSteps: config2.stallMinSteps,
|
|
17175
17867
|
stallTokenThreshold: config2.stallTokenThreshold,
|
|
17176
|
-
cwd: input.directory
|
|
17868
|
+
cwd: input.directory,
|
|
17869
|
+
peerMessageLimit: config2.peerMessageLimit,
|
|
17870
|
+
peerMessageWindowMs: config2.peerMessageWindowMs
|
|
17177
17871
|
});
|
|
17178
17872
|
watchdog.start();
|
|
17179
17873
|
return {
|
|
@@ -17241,6 +17935,22 @@ var plugin = async (input) => {
|
|
|
17241
17935
|
});
|
|
17242
17936
|
}
|
|
17243
17937
|
}
|
|
17938
|
+
const member = db.query(`SELECT tm.team_id, tm.name FROM team_member tm
|
|
17939
|
+
JOIN team t ON tm.team_id = t.id
|
|
17940
|
+
WHERE tm.session_id = ? AND t.status = 'active'`).get(sessionID);
|
|
17941
|
+
if (member) {
|
|
17942
|
+
const staleThreshold = Date.now() - 5000;
|
|
17943
|
+
const peerMsgs = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = ? AND delivered = 0 AND time_created < ?").get(member.team_id, member.name, staleThreshold);
|
|
17944
|
+
if (peerMsgs.c > 0) {
|
|
17945
|
+
log(`wake-peer: ${member.name} has ${peerMsgs.c} pending peer messages`);
|
|
17946
|
+
client3.session.promptAsync({
|
|
17947
|
+
sessionID,
|
|
17948
|
+
parts: [{ type: "text", text: `[System: ${peerMsgs.c} new message(s) from teammates]` }]
|
|
17949
|
+
}).catch((err) => {
|
|
17950
|
+
log(`wake-peer:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
17951
|
+
});
|
|
17952
|
+
}
|
|
17953
|
+
}
|
|
17244
17954
|
}
|
|
17245
17955
|
}
|
|
17246
17956
|
if (event.type === "session.created") {
|
|
@@ -17343,6 +18053,8 @@ var plugin = async (input) => {
|
|
|
17343
18053
|
async execute(args, ctx) {
|
|
17344
18054
|
const result = await executeTeamMessage(deps, args, ctx.sessionID);
|
|
17345
18055
|
progressTracker.recordMessage(ctx.sessionID);
|
|
18056
|
+
if (args.to !== "lead")
|
|
18057
|
+
progressTracker.recordPeerMessage(ctx.sessionID);
|
|
17346
18058
|
ctx.metadata({ title: `Message \u2192 ${args.to}` });
|
|
17347
18059
|
return result;
|
|
17348
18060
|
}
|