@hueyexe/opencode-ensemble 0.13.0 → 0.13.2
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 +9 -5
- package/dist/index.js +190 -46
- package/package.json +3 -2
- package/social-preview.png +0 -0
- package/src/dashboard-html.ts +20 -15
- package/src/dashboard-js-part1.ts +34 -0
- package/src/dashboard-js-part2.ts +40 -14
- package/src/dashboard-js-part3.ts +45 -5
- package/src/dashboard.ts +16 -1
- package/src/hooks.ts +16 -1
- package/src/index.ts +13 -4
- package/src/messaging.ts +11 -0
- package/src/recovery.ts +7 -1
- package/src/schema.ts +2 -0
- package/src/tools/team-broadcast.ts +9 -2
- package/src/tools/team-message.ts +6 -1
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="social-preview.png" alt="OpenCode Ensemble - Parallel agents. One coordinated team." width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# OpenCode Ensemble
|
|
2
6
|
|
|
3
7
|
[](https://www.npmjs.com/package/@hueyexe/opencode-ensemble)
|
|
4
8
|
[](https://www.npmjs.com/package/@hueyexe/opencode-ensemble)
|
|
5
|
-
[]()
|
|
6
10
|
[]()
|
|
7
11
|
[]()
|
|
8
12
|
[](./LICENSE)
|
|
@@ -15,7 +19,7 @@ Plugin built on the public OpenCode SDK. No internal dependencies.
|
|
|
15
19
|
|
|
16
20
|
```json
|
|
17
21
|
{
|
|
18
|
-
"plugin": ["@hueyexe/opencode-ensemble@0.13.
|
|
22
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.13.2"]
|
|
19
23
|
}
|
|
20
24
|
```
|
|
21
25
|
|
|
@@ -136,7 +140,7 @@ Add to your OpenCode config with a pinned version. Project-level or global.
|
|
|
136
140
|
|
|
137
141
|
```json
|
|
138
142
|
{
|
|
139
|
-
"plugin": ["@hueyexe/opencode-ensemble@0.13.
|
|
143
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.13.2"]
|
|
140
144
|
}
|
|
141
145
|
```
|
|
142
146
|
|
|
@@ -144,7 +148,7 @@ Add to your OpenCode config with a pinned version. Project-level or global.
|
|
|
144
148
|
|
|
145
149
|
```json
|
|
146
150
|
{
|
|
147
|
-
"plugin": ["@hueyexe/opencode-ensemble@0.13.
|
|
151
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.13.2"]
|
|
148
152
|
}
|
|
149
153
|
```
|
|
150
154
|
|
|
@@ -406,7 +410,7 @@ Same coordination model (shared tasks, peer messaging, lead coordination) with s
|
|
|
406
410
|
```bash
|
|
407
411
|
bun install
|
|
408
412
|
bun run typecheck
|
|
409
|
-
bun test #
|
|
413
|
+
bun test # 508 tests
|
|
410
414
|
bun run build
|
|
411
415
|
```
|
|
412
416
|
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __returnValue = (v) => v;
|
|
4
|
-
function __exportSetter(name, newValue) {
|
|
5
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
-
}
|
|
7
3
|
var __export = (target, all) => {
|
|
8
4
|
for (var name in all)
|
|
9
5
|
__defProp(target, name, {
|
|
10
6
|
get: all[name],
|
|
11
7
|
enumerable: true,
|
|
12
8
|
configurable: true,
|
|
13
|
-
set:
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
14
10
|
});
|
|
15
11
|
};
|
|
16
12
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -15454,7 +15450,8 @@ var MIGRATIONS = [
|
|
|
15454
15450
|
`ALTER TABLE team_member ADD COLUMN plan_approval TEXT NOT NULL DEFAULT 'none'
|
|
15455
15451
|
CHECK(plan_approval IN ('none', 'pending', 'approved', 'rejected'));`,
|
|
15456
15452
|
`ALTER TABLE team ADD COLUMN lead_agent TEXT;`,
|
|
15457
|
-
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT
|
|
15453
|
+
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT;`,
|
|
15454
|
+
`ALTER TABLE team_member ADD COLUMN reported_to_lead INTEGER NOT NULL DEFAULT 0;`
|
|
15458
15455
|
];
|
|
15459
15456
|
function applyMigrations(db) {
|
|
15460
15457
|
const { user_version: current } = db.query("PRAGMA user_version").get();
|
|
@@ -15576,6 +15573,10 @@ function getUndeliveredMessages(db, teamId) {
|
|
|
15576
15573
|
function markDelivered(db, messageId) {
|
|
15577
15574
|
db.run("UPDATE team_message SET delivered = 1 WHERE id = ?", [messageId]);
|
|
15578
15575
|
}
|
|
15576
|
+
function hasReportedCompletion(db, teamId, memberName) {
|
|
15577
|
+
const row = db.query("SELECT reported_to_lead FROM team_member WHERE team_id = ? AND name = ?").get(teamId, memberName);
|
|
15578
|
+
return row?.reported_to_lead === 1;
|
|
15579
|
+
}
|
|
15579
15580
|
|
|
15580
15581
|
// src/recovery.ts
|
|
15581
15582
|
init_merge_helper();
|
|
@@ -15643,6 +15644,10 @@ async function recoverUndeliveredMessages(db, client3, registry2) {
|
|
|
15643
15644
|
}
|
|
15644
15645
|
if (!recipientSessionId)
|
|
15645
15646
|
continue;
|
|
15647
|
+
if (hasReportedCompletion(db, team.id, msg.to_name)) {
|
|
15648
|
+
markDelivered(db, msg.id);
|
|
15649
|
+
continue;
|
|
15650
|
+
}
|
|
15646
15651
|
try {
|
|
15647
15652
|
await client3.session.promptAsync({
|
|
15648
15653
|
sessionID: recipientSessionId,
|
|
@@ -15778,10 +15783,16 @@ function handleSessionStatusEvent(db, registry2, sessionId, status) {
|
|
|
15778
15783
|
if (member.status === newStatus)
|
|
15779
15784
|
return;
|
|
15780
15785
|
db.run("UPDATE team_member SET status = ?, execution_status = 'idle', time_updated = ? WHERE team_id = ? AND name = ?", [newStatus, Date.now(), entry.teamId, entry.memberName]);
|
|
15786
|
+
if (member.status === "busy" && newStatus === "ready") {
|
|
15787
|
+
const leadMsgCount = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND from_name = ? AND to_name = 'lead'").get(entry.teamId, entry.memberName).c;
|
|
15788
|
+
if (leadMsgCount > 0) {
|
|
15789
|
+
db.run("UPDATE team_member SET reported_to_lead = 1 WHERE team_id = ? AND name = ?", [entry.teamId, entry.memberName]);
|
|
15790
|
+
}
|
|
15791
|
+
}
|
|
15781
15792
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: newStatus };
|
|
15782
15793
|
} else if (status === "busy") {
|
|
15783
15794
|
if (member.status === "ready" || member.status === "error") {
|
|
15784
|
-
db.run("UPDATE team_member SET status = 'busy', execution_status = 'running', time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), entry.teamId, entry.memberName]);
|
|
15795
|
+
db.run("UPDATE team_member SET status = 'busy', execution_status = 'running', reported_to_lead = 0, time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), entry.teamId, entry.memberName]);
|
|
15785
15796
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: "busy" };
|
|
15786
15797
|
}
|
|
15787
15798
|
if (member.status === "shutdown_requested") {
|
|
@@ -16256,7 +16267,7 @@ var DASHBOARD_HEAD = `<!DOCTYPE html>
|
|
|
16256
16267
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16257
16268
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
16258
16269
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
16259
|
-
<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:'#
|
|
16270
|
+
<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:'#aab4c6',400:'#8a96aa',500:'#7b879b'}},fontFamily:{sans:['Inter','system-ui','sans-serif'],mono:['JetBrains Mono','monospace']}}}}</script>
|
|
16260
16271
|
<style>
|
|
16261
16272
|
@media(prefers-reduced-motion:no-preference){
|
|
16262
16273
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
@@ -16268,8 +16279,10 @@ var DASHBOARD_HEAD = `<!DOCTYPE html>
|
|
|
16268
16279
|
.hl{animation:hl 1.5s ease-out}
|
|
16269
16280
|
.shimmer{background:linear-gradient(90deg,#22c55e 0%,#4ade80 50%,#22c55e 100%)!important;background-size:200% 100%;animation:shimmer 1.5s ease-in-out}
|
|
16270
16281
|
}
|
|
16282
|
+
@media(prefers-reduced-motion:reduce){*,::before,::after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
|
16271
16283
|
.scroll::-webkit-scrollbar{width:5px}.scroll::-webkit-scrollbar-track{background:transparent}.scroll::-webkit-scrollbar-thumb{background:#2a3144;border-radius:3px}
|
|
16272
16284
|
details summary::-webkit-details-marker{display:none}details summary{list-style:none}
|
|
16285
|
+
:focus-visible{outline:2px solid rgba(96,165,250,.85);outline-offset:2px}
|
|
16273
16286
|
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}
|
|
16274
16287
|
.xp{max-height:0;overflow:hidden;transition:max-height .3s ease-out}.xp-open{max-height:3000px;transition:max-height .5s ease-in}
|
|
16275
16288
|
.card-sel{outline:2px solid rgba(59,130,246,.4);outline-offset:1px}
|
|
@@ -16288,13 +16301,13 @@ select{-webkit-appearance:none;appearance:none;background-image:url("data:image/
|
|
|
16288
16301
|
</style>
|
|
16289
16302
|
</head>
|
|
16290
16303
|
<body class="bg-base-950 text-txt-100 min-h-screen antialiased font-sans">
|
|
16291
|
-
<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">
|
|
16292
|
-
<div class="flex items-center gap-3">
|
|
16304
|
+
<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-3 sm:px-4 z-50">
|
|
16305
|
+
<div class="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
|
16293
16306
|
<span class="font-mono font-semibold text-[13px] tracking-[.08em] text-txt-200">ensemble</span>
|
|
16294
16307
|
<span class="text-base-700">|</span>
|
|
16295
|
-
<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>
|
|
16308
|
+
<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 max-w-[180px] sm:max-w-[320px] min-w-0"></select>
|
|
16296
16309
|
</div>
|
|
16297
|
-
<div class="flex items-center gap-4">
|
|
16310
|
+
<div class="flex items-center gap-2 sm:gap-4 shrink-0">
|
|
16298
16311
|
<div id="hring" class="w-6 h-6 rounded-full" title="Team health"></div>
|
|
16299
16312
|
<div class="flex items-center gap-2">
|
|
16300
16313
|
<span id="clk" class="text-[11px] text-txt-400 font-mono"></span>
|
|
@@ -16304,25 +16317,28 @@ select{-webkit-appearance:none;appearance:none;background-image:url("data:image/
|
|
|
16304
16317
|
</div>
|
|
16305
16318
|
</div>
|
|
16306
16319
|
</header>
|
|
16307
|
-
<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>
|
|
16320
|
+
<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-3 sm:px-4 gap-3 sm:gap-4 text-[11px] text-txt-300 z-40 overflow-x-auto scroll whitespace-nowrap"></div>
|
|
16308
16321
|
<main class="pt-[76px] px-4 pb-16">
|
|
16309
16322
|
<div id="empty" class="hidden flex-col items-center justify-center h-[70vh] gap-3">
|
|
16310
16323
|
<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>
|
|
16311
16324
|
<div class="text-txt-400 text-sm">Waiting for a team</div>
|
|
16312
16325
|
<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>
|
|
16313
16326
|
</div>
|
|
16314
|
-
<div id="content" class="hidden">
|
|
16315
|
-
<
|
|
16316
|
-
<div class="grid grid-cols-1
|
|
16317
|
-
<div id="
|
|
16318
|
-
<div
|
|
16327
|
+
<div id="content" class="hidden max-w-[1600px] mx-auto">
|
|
16328
|
+
<section id="attention" aria-label="Team attention" class="mb-3"></section>
|
|
16329
|
+
<div class="grid grid-cols-1 xl:grid-cols-[minmax(360px,1.35fr)_minmax(320px,.8fr)] gap-4 items-start">
|
|
16330
|
+
<section aria-label="Agent roster"><div id="agents" class="grid gap-2" style="grid-template-columns:repeat(auto-fill,minmax(300px,1fr))"></div></section>
|
|
16331
|
+
<div class="grid grid-cols-1 gap-4">
|
|
16332
|
+
<section aria-label="Task board"><div id="tasks" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div></section>
|
|
16333
|
+
<section aria-label="Activity feed"><div id="activity" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div></section>
|
|
16334
|
+
</div>
|
|
16319
16335
|
</div>
|
|
16320
16336
|
</div>
|
|
16321
16337
|
</main>
|
|
16322
|
-
<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>
|
|
16323
|
-
<div id="sco" onclick="
|
|
16338
|
+
<div id="tl" aria-label="Event timeline" 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>
|
|
16339
|
+
<div id="sco" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="shortcuts-title" tabindex="-1" onclick="closeShortcuts()">
|
|
16324
16340
|
<div class="bg-base-900 border border-base-800 rounded-lg p-6 max-w-sm" onclick="event.stopPropagation()">
|
|
16325
|
-
<div class="text-txt-200 font-semibold text-sm mb-4">Keyboard Shortcuts</div>
|
|
16341
|
+
<div id="shortcuts-title" class="text-txt-200 font-semibold text-sm mb-4">Keyboard Shortcuts</div>
|
|
16326
16342
|
<div class="grid grid-cols-2 gap-y-2 gap-x-6 text-[12px]">
|
|
16327
16343
|
<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>
|
|
16328
16344
|
<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>
|
|
@@ -16334,7 +16350,7 @@ select{-webkit-appearance:none;appearance:none;background-image:url("data:image/
|
|
|
16334
16350
|
</div>
|
|
16335
16351
|
</div>
|
|
16336
16352
|
<div id="drawer-bg" onclick="closeDrawer()"></div>
|
|
16337
|
-
<div id="drawer" class="scroll p-4"></div>`;
|
|
16353
|
+
<div id="drawer" class="scroll p-4" tabindex="-1" inert role="dialog" aria-modal="true" aria-labelledby="drawer-title" aria-hidden="true"><h2 id="drawer-title" class="sr-only">Agent detail</h2></div>`;
|
|
16338
16354
|
|
|
16339
16355
|
// src/dashboard-js-part1.ts
|
|
16340
16356
|
var DASHBOARD_JS_PART1 = `
|
|
@@ -16408,6 +16424,40 @@ function deriveHealth(t){
|
|
|
16408
16424
|
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};
|
|
16409
16425
|
}
|
|
16410
16426
|
|
|
16427
|
+
function lastMessageFor(name,msgs){return msgs.filter(m=>m.fromName===name||m.toName===name).sort((a,b)=>b.timeCreated-a.timeCreated)[0]||null}
|
|
16428
|
+
function activeTaskFor(name,tasks){return tasks.find(x=>x.assignee===name&&x.status==='in_progress')||tasks.find(x=>x.assignee===name&&x.status==='blocked')||null}
|
|
16429
|
+
function blockedTaskFor(name,tasks){return tasks.find(x=>x.assignee===name&&x.status==='blocked')||null}
|
|
16430
|
+
|
|
16431
|
+
function rankAgent(a,b,t){
|
|
16432
|
+
const tasks=t?.tasks||[],msgs=t?.messages||[];
|
|
16433
|
+
const score=m=>{
|
|
16434
|
+
const lm=lastMessageFor(m.name,msgs),bt=blockedTaskFor(m.name,tasks);
|
|
16435
|
+
let s=0;
|
|
16436
|
+
if(m.status==='error')s-=1000;
|
|
16437
|
+
if(bt)s-=800;
|
|
16438
|
+
if(m.status==='shutdown_requested')s-=650;
|
|
16439
|
+
if(m.status==='busy')s-=500;
|
|
16440
|
+
if(m.status==='ready'&&activeTaskFor(m.name,tasks))s-=350;
|
|
16441
|
+
if(lm)s-=Math.max(0,200-Math.floor((Date.now()-lm.timeCreated)/60000));
|
|
16442
|
+
if(m.status==='shutdown')s+=700;
|
|
16443
|
+
return s;
|
|
16444
|
+
};
|
|
16445
|
+
const d=score(a)-score(b);return d||String(a.name).localeCompare(String(b.name));
|
|
16446
|
+
}
|
|
16447
|
+
|
|
16448
|
+
function deriveAttention(t){
|
|
16449
|
+
const tasks=t.tasks||[],msgs=t.messages||[];
|
|
16450
|
+
const blocked=tasks.filter(x=>x.status==='blocked'),running=tasks.filter(x=>x.status==='in_progress');
|
|
16451
|
+
const errored=(t.members||[]).filter(m=>m.status==='error');
|
|
16452
|
+
const stopping=(t.members||[]).filter(m=>m.status==='shutdown_requested');
|
|
16453
|
+
const latest=msgs[0]||null;
|
|
16454
|
+
const items=[];
|
|
16455
|
+
errored.forEach(m=>items.push({kind:'Agent error',label:m.name,detail:m.executionStatus||m.status,color:'red'}));
|
|
16456
|
+
blocked.forEach(x=>items.push({kind:'Blocked task',label:x.assignee||'unassigned',detail:x.content,color:'amber'}));
|
|
16457
|
+
stopping.forEach(m=>items.push({kind:'Stopping',label:m.name,detail:'shutdown requested',color:'amber'}));
|
|
16458
|
+
return{items,running,latest,blocked,errored};
|
|
16459
|
+
}
|
|
16460
|
+
|
|
16411
16461
|
function deriveSparkline(name,msgs){
|
|
16412
16462
|
const mine=msgs.filter(m=>m.fromName===name).map(m=>m.timeCreated).sort();
|
|
16413
16463
|
if(mine.length<2)return '';
|
|
@@ -16483,17 +16533,32 @@ function rSum(t){
|
|
|
16483
16533
|
);
|
|
16484
16534
|
}
|
|
16485
16535
|
|
|
16536
|
+
function rAttention(t){
|
|
16537
|
+
const el=document.getElementById('attention'),a=deriveAttention(t),h=deriveHealth(t),tk=t.tasks||[];
|
|
16538
|
+
const tone=a.items.length?'border-amber-500/30 bg-amber-500/[0.05]':'border-emerald-500/20 bg-emerald-500/[0.035]';
|
|
16539
|
+
const lead=a.items.length?'Needs attention':'No blockers detected';
|
|
16540
|
+
const chips=[chip(h.w+' working','blue'),chip(a.running.length+' active tasks','green'),chip(a.blocked.length+' blocked',a.blocked.length?'amber':'muted'),chip(tk.filter(x=>x.status==='pending').length+' pending','muted')];
|
|
16541
|
+
let html='<div class="rounded-lg border '+tone+' px-3 py-2 flex flex-col md:flex-row md:items-center gap-2 md:gap-4">';
|
|
16542
|
+
html+='<div class="min-w-[180px]"><div class="text-[10px] uppercase tracking-[.16em] text-txt-500">Team attention</div><div class="text-[14px] font-semibold text-txt-100">'+lead+'</div></div>';
|
|
16543
|
+
html+='<div class="flex flex-wrap gap-1.5">'+chips.join('')+'</div>';
|
|
16544
|
+
if(a.items.length){html+='<div class="flex-1 grid gap-1 md:grid-cols-2">'+a.items.slice(0,4).map(x=>'<div class="text-[12px] text-txt-200 truncate">'+chip(E(x.kind),x.color)+' <span class="font-mono">'+E(x.label)+'</span> <span class="text-txt-400">'+E(x.detail)+'</span></div>').join('')+'</div>'}
|
|
16545
|
+
else if(a.latest){html+='<div class="text-[12px] text-txt-400 truncate md:ml-auto">Latest: <span class="text-txt-200">'+E(a.latest.fromName)+'</span> '+relT(a.latest.timeCreated)+'</div>'}
|
|
16546
|
+
html+='</div>';
|
|
16547
|
+
patch(el,html);
|
|
16548
|
+
}
|
|
16549
|
+
|
|
16486
16550
|
function rAgents(t){
|
|
16487
16551
|
const el=document.getElementById('agents'),mm=t.members||[];
|
|
16488
16552
|
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}
|
|
16489
|
-
const n=Date.now(),msgs=t.messages||[],sorted=[...mm].sort((a,b)=>
|
|
16553
|
+
const n=Date.now(),msgs=t.messages||[],sorted=[...mm].sort((a,b)=>rankAgent(a,b,t));
|
|
16490
16554
|
const html=sorted.map((m,idx)=>{
|
|
16491
|
-
const s=si(m.status),task=(t.tasks||[])
|
|
16555
|
+
const s=si(m.status),task=activeTaskFor(m.name,t.tasks||[]),msg=lastMessageFor(m.name,msgs),blocked=blockedTaskFor(m.name,t.tasks||[]);
|
|
16492
16556
|
const d=D(n-m.timeUpdated),mi=msg?relT(msg.timeCreated):'\\u2014';
|
|
16493
16557
|
const tt=task?.content,tr=tt&&tt.length>90?tt.slice(0,90)+'\\u2026':tt;
|
|
16558
|
+
const mp=msg?(msg.content.length>90?msg.content.slice(0,90)+'\\u2026':msg.content):'';
|
|
16494
16559
|
const spark=deriveSparkline(m.name,msgs);
|
|
16495
16560
|
const isSel=selCard===idx;
|
|
16496
|
-
return '<
|
|
16561
|
+
return '<button type="button" class="text-left rounded-lg border '+s.c+' p-3 transition-all duration-300 cursor-pointer hover:border-base-600 focus-visible:border-blue-400'+(isSel?' card-sel':'')+'" data-card="'+E(m.name)+'" onclick="openDrawer(this.dataset.card)" onkeydown="if(event.key===\\'Enter\\'||event.key===\\' \\'){event.preventDefault();openDrawer(this.dataset.card)}">'+
|
|
16497
16562
|
'<div class="flex items-center gap-2">'+
|
|
16498
16563
|
'<span class="w-[8px] h-[8px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+' shrink-0"></span>'+
|
|
16499
16564
|
'<span class="font-mono font-semibold text-[14px] truncate">'+E(m.name)+'</span>'+
|
|
@@ -16502,11 +16567,12 @@ function rAgents(t){
|
|
|
16502
16567
|
'<span class="text-[10px] text-txt-500 ml-auto shrink-0">'+E(m.agent)+'</span>'+
|
|
16503
16568
|
(m.model?chip(E(m.model),'muted'):'')+
|
|
16504
16569
|
'</div>'+
|
|
16505
|
-
(tr?'<div class="mt-2 text-[13px] text-txt-200 leading-snug truncate">'+E(tr)+'</div>':'')+
|
|
16570
|
+
(tr?'<div class="mt-2 text-[13px] text-txt-200 leading-snug truncate">'+(blocked?'<span class="text-amber-400">Blocked: </span>':'Current task: ')+E(tr)+'</div>':'<div class="mt-2 text-[13px] text-txt-500 leading-snug">No active task</div>')+
|
|
16571
|
+
(mp?'<div class="mt-1 text-[12px] text-txt-400 truncate">Latest: '+E(mp)+'</div>':'')+
|
|
16506
16572
|
'<div class="mt-2 flex items-center gap-1.5 flex-wrap">'+
|
|
16507
|
-
chip(d,'muted')+chip('msg '+mi,'muted')+
|
|
16573
|
+
chip('status '+d,'muted')+chip('msg '+mi,'muted')+chip(E(m.executionStatus||m.status),m.status==='busy'?'blue':m.status==='error'?'red':'muted')+
|
|
16508
16574
|
(m.worktreeBranch?chip(E(m.worktreeBranch),'muted'):'')+
|
|
16509
|
-
'</div></
|
|
16575
|
+
'</div></button>';
|
|
16510
16576
|
}).join('');
|
|
16511
16577
|
patch(el,html);
|
|
16512
16578
|
}
|
|
@@ -16518,8 +16584,8 @@ function openDrawer(name){
|
|
|
16518
16584
|
var h='';
|
|
16519
16585
|
// Header
|
|
16520
16586
|
h+='<div class="flex items-center justify-between mb-4">';
|
|
16521
|
-
h+='<div class="flex items-center gap-2"><span class="w-[10px] h-[10px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+'"></span><
|
|
16522
|
-
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>';
|
|
16587
|
+
h+='<div class="flex items-center gap-2"><span class="w-[10px] h-[10px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+'"></span><h2 id="drawer-title" class="font-mono font-semibold text-[16px]">'+E(m.name)+'</h2><span class="text-[11px] px-2 py-[2px] rounded '+s.t+' bg-base-800/80">'+s.l+'</span></div>';
|
|
16588
|
+
h+='<button id="drawer-close" aria-label="Close agent detail" 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>';
|
|
16523
16589
|
h+='</div>';
|
|
16524
16590
|
// Meta chips
|
|
16525
16591
|
var meta=[];
|
|
@@ -16566,13 +16632,22 @@ function openDrawer(name){
|
|
|
16566
16632
|
h+='</div></div>';
|
|
16567
16633
|
});
|
|
16568
16634
|
}
|
|
16569
|
-
document.getElementById('drawer')
|
|
16570
|
-
|
|
16635
|
+
var drawer=document.getElementById('drawer');
|
|
16636
|
+
drawer.innerHTML=h;
|
|
16637
|
+
drawer.classList.add('open');
|
|
16638
|
+
drawer.setAttribute('aria-hidden','false');
|
|
16639
|
+
drawer.inert=false;
|
|
16640
|
+
setBackgroundInert(true);
|
|
16641
|
+
drawer.focus();
|
|
16571
16642
|
document.getElementById('drawer-bg').classList.add('open');
|
|
16572
16643
|
}
|
|
16573
16644
|
|
|
16574
16645
|
function closeDrawer(){
|
|
16575
|
-
document.getElementById('drawer')
|
|
16646
|
+
var drawer=document.getElementById('drawer');
|
|
16647
|
+
drawer.classList.remove('open');
|
|
16648
|
+
drawer.setAttribute('aria-hidden','true');
|
|
16649
|
+
drawer.inert=true;
|
|
16650
|
+
if(!modalOpen())setBackgroundInert(false);
|
|
16576
16651
|
document.getElementById('drawer-bg').classList.remove('open');
|
|
16577
16652
|
}
|
|
16578
16653
|
|
|
@@ -16591,7 +16666,8 @@ function rTasks(t){
|
|
|
16591
16666
|
const icl=x.status==='completed'?'text-emerald-500':x.status==='in_progress'?'text-blue-400':'text-txt-500';
|
|
16592
16667
|
const p=PR[x.priority]||PR.low,cn=x.content.length>55?x.content.slice(0,55)+'\\u2026':x.content;
|
|
16593
16668
|
let dep='';
|
|
16594
|
-
|
|
16669
|
+
const depIds=Array.isArray(x.dependsOn)?x.dependsOn:(x.dependsOn?[String(x.dependsOn)]:[]);
|
|
16670
|
+
if(depIds.length){const labels=depIds.map(id=>{const parent=taskMap[id];return parent?parent.content.slice(0,42):id}).join(', ');dep='<div class="text-[10px] text-txt-500 mt-0.5">blocked by: '+E(labels)+'</div>'}
|
|
16595
16671
|
return '<div class="flex items-start gap-2 py-1.5 px-1 rounded hover:bg-base-800/40 transition-colors">'+
|
|
16596
16672
|
'<span class="'+icl+' text-[11px] mt-[2px] shrink-0">'+ic+'</span>'+
|
|
16597
16673
|
'<div class="flex-1 min-w-0"><div class="text-[13px] text-txt-200 truncate">'+E(cn)+'</div>'+
|
|
@@ -16618,7 +16694,7 @@ function rActivity(t){
|
|
|
16618
16694
|
if(!msgs.length){patch(el,'<div class="text-txt-500 text-sm py-6 text-center">Waiting for agent messages...</div>');return}
|
|
16619
16695
|
const sp=saveSc(el);
|
|
16620
16696
|
const newCount=msgs.length,hasNew=newCount>prevMC&&prevMC>0;
|
|
16621
|
-
let html='<div class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider
|
|
16697
|
+
let html='<div class="flex items-center gap-2 mb-3"><span class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider">Activity feed</span>'+chip(msgs.length+' msgs','muted')+'</div>';
|
|
16622
16698
|
html+='<div class="max-h-[50vh] overflow-y-auto scroll space-y-2">';
|
|
16623
16699
|
msgs.slice(0,30).forEach(function(m,mi){
|
|
16624
16700
|
const isNew=hasNew&&mi<(newCount-prevMC);
|
|
@@ -16631,7 +16707,7 @@ function rActivity(t){
|
|
|
16631
16707
|
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';
|
|
16632
16708
|
const initial=m.fromName.charAt(0).toUpperCase();
|
|
16633
16709
|
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';
|
|
16634
|
-
html+='<div class="'+align+(isNew?' hl':'')+' cursor-pointer" data-msg="'+E(m.id)+'" onclick="toggleMsg(\\'
|
|
16710
|
+
html+='<div class="'+align+(isNew?' hl':'')+' cursor-pointer" role="button" tabindex="0" aria-expanded="'+(isExp?'true':'false')+'" data-msg="'+E(m.id)+'" onclick="toggleMsg(this.dataset.msg)" onkeydown="if(event.key===\\'Enter\\'||event.key===\\' \\'){event.preventDefault();toggleMsg(this.dataset.msg)}">';
|
|
16635
16711
|
html+='<div class="flex items-start gap-2">';
|
|
16636
16712
|
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>';
|
|
16637
16713
|
html+='<div class="flex-1 min-w-0 rounded-xl border '+bubbleBg+' px-3 py-2">';
|
|
@@ -16678,16 +16754,49 @@ function render(){
|
|
|
16678
16754
|
const empty=document.getElementById('empty'),content=document.getElementById('content');
|
|
16679
16755
|
if(!t){empty.classList.remove('hidden');empty.classList.add('flex');content.classList.add('hidden');document.getElementById('tl').classList.add('hidden');return}
|
|
16680
16756
|
empty.classList.add('hidden');empty.classList.remove('flex');content.classList.remove('hidden');
|
|
16681
|
-
rHealth(t);rSum(t);rAgents(t);rTasks(t);rActivity(t);rTimeline(t);
|
|
16757
|
+
rHealth(t);rSum(t);rAttention(t);rAgents(t);rTasks(t);rActivity(t);rTimeline(t);
|
|
16682
16758
|
}
|
|
16683
16759
|
|
|
16684
16760
|
function conn(ok){
|
|
16685
16761
|
document.getElementById('cd').className='w-[7px] h-[7px] rounded-full '+(ok?'bg-emerald-500 pulse':'bg-red-500');
|
|
16686
|
-
document.getElementById('ct').textContent=ok?D(Date.now()-pollT)+' ago':'
|
|
16762
|
+
document.getElementById('ct').textContent=ok?D(Date.now()-pollT)+' ago':'reconnecting to dashboard state';
|
|
16687
16763
|
}
|
|
16688
16764
|
|
|
16689
16765
|
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)}}
|
|
16690
16766
|
|
|
16767
|
+
function setBackgroundInert(locked){
|
|
16768
|
+
document.querySelectorAll('header,main,#sum,#tl').forEach(function(el){
|
|
16769
|
+
el.inert=locked;
|
|
16770
|
+
if(locked)el.setAttribute('aria-hidden','true');
|
|
16771
|
+
else el.removeAttribute('aria-hidden');
|
|
16772
|
+
});
|
|
16773
|
+
}
|
|
16774
|
+
|
|
16775
|
+
function modalOpen(){return document.getElementById('sco').classList.contains('show')||document.getElementById('drawer').classList.contains('open')}
|
|
16776
|
+
|
|
16777
|
+
function trapFocus(root,e){
|
|
16778
|
+
const nodes=[...root.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])')].filter(function(el){return !el.disabled&&!el.inert&&el.offsetParent!==null});
|
|
16779
|
+
if(!nodes.length){e.preventDefault();root.focus();return}
|
|
16780
|
+
const first=nodes[0],last=nodes[nodes.length-1];
|
|
16781
|
+
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();return}
|
|
16782
|
+
if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();return}
|
|
16783
|
+
}
|
|
16784
|
+
|
|
16785
|
+
function openShortcuts(){
|
|
16786
|
+
const el=document.getElementById('sco');
|
|
16787
|
+
setBackgroundInert(true);
|
|
16788
|
+
el.classList.add('show');
|
|
16789
|
+
el.setAttribute('aria-hidden','false');
|
|
16790
|
+
document.getElementById('sco').focus();
|
|
16791
|
+
}
|
|
16792
|
+
|
|
16793
|
+
function closeShortcuts(){
|
|
16794
|
+
const el=document.getElementById('sco');
|
|
16795
|
+
el.classList.remove('show');
|
|
16796
|
+
el.setAttribute('aria-hidden','true');
|
|
16797
|
+
if(!modalOpen())setBackgroundInert(false);
|
|
16798
|
+
}
|
|
16799
|
+
|
|
16691
16800
|
// Clock update every second
|
|
16692
16801
|
setInterval(function(){var t=cur();if(t)rClock(t);if(fails<3)conn(true)},1000);
|
|
16693
16802
|
|
|
@@ -16696,11 +16805,18 @@ setInterval(poll,2500);
|
|
|
16696
16805
|
|
|
16697
16806
|
// Keyboard shortcuts
|
|
16698
16807
|
document.addEventListener('keydown',function(e){
|
|
16808
|
+
const shortcutsOpen=document.getElementById('sco').classList.contains('show');
|
|
16809
|
+
if(shortcutsOpen&&e.key==='Tab'){trapFocus(document.getElementById('sco'),e);return}
|
|
16810
|
+
if(shortcutsOpen&&e.key==='Escape'){e.preventDefault();closeShortcuts();return}
|
|
16811
|
+
const drawerOpen=document.getElementById('drawer').classList.contains('open');
|
|
16812
|
+
if(drawerOpen&&e.key==='Tab'){trapFocus(document.getElementById('drawer'),e);return}
|
|
16813
|
+
if(drawerOpen&&e.key==='Escape'){e.preventDefault();closeDrawer();return}
|
|
16814
|
+
if(drawerOpen&&e.key==='?'){e.preventDefault();return}
|
|
16699
16815
|
if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT'||e.target.tagName==='TEXTAREA')return;
|
|
16700
16816
|
var t=cur();if(!t)return;
|
|
16701
|
-
var mm=t.members||[];
|
|
16702
|
-
if(e.key==='?'){e.preventDefault();
|
|
16703
|
-
if(e.key==='Escape'){closeDrawer();expMsgs.clear();selCard=-1;
|
|
16817
|
+
var mm=[...(t.members||[])].sort((a,b)=>rankAgent(a,b,t));
|
|
16818
|
+
if(e.key==='?'){e.preventDefault();shortcutsOpen?closeShortcuts():openShortcuts();return}
|
|
16819
|
+
if(e.key==='Escape'){closeDrawer();expMsgs.clear();selCard=-1;closeShortcuts();render();return}
|
|
16704
16820
|
if(e.key==='j'&&mm.length){e.preventDefault();selCard=Math.min(selCard+1,mm.length-1);render();return}
|
|
16705
16821
|
if(e.key==='k'&&mm.length){e.preventDefault();selCard=Math.max(selCard-1,0);render();return}
|
|
16706
16822
|
if(e.key==='Enter'&&selCard>=0&&selCard<mm.length){e.preventDefault();openDrawer(mm[selCard].name);return}
|
|
@@ -16730,6 +16846,20 @@ function jsonResponse(data, status = 200) {
|
|
|
16730
16846
|
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
|
|
16731
16847
|
});
|
|
16732
16848
|
}
|
|
16849
|
+
function parseDependsOn(value) {
|
|
16850
|
+
if (!value)
|
|
16851
|
+
return [];
|
|
16852
|
+
try {
|
|
16853
|
+
const parsed = JSON.parse(value);
|
|
16854
|
+
if (Array.isArray(parsed))
|
|
16855
|
+
return parsed.filter((item) => typeof item === "string");
|
|
16856
|
+
if (typeof parsed === "string")
|
|
16857
|
+
return [parsed];
|
|
16858
|
+
} catch {
|
|
16859
|
+
return [value];
|
|
16860
|
+
}
|
|
16861
|
+
return [];
|
|
16862
|
+
}
|
|
16733
16863
|
function buildState(db) {
|
|
16734
16864
|
const teams = db.query("SELECT id, name, status, lead_agent, time_created, time_updated FROM team ORDER BY time_created DESC").all();
|
|
16735
16865
|
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 = ?");
|
|
@@ -16761,7 +16891,7 @@ function buildState(db) {
|
|
|
16761
16891
|
status: tk.status,
|
|
16762
16892
|
priority: tk.priority,
|
|
16763
16893
|
assignee: tk.assignee,
|
|
16764
|
-
dependsOn: tk.depends_on,
|
|
16894
|
+
dependsOn: parseDependsOn(tk.depends_on),
|
|
16765
16895
|
timeCreated: tk.time_created,
|
|
16766
16896
|
timeUpdated: tk.time_updated
|
|
16767
16897
|
})),
|
|
@@ -17189,6 +17319,9 @@ async function executeTeamMessage(deps, args, sessionId) {
|
|
|
17189
17319
|
});
|
|
17190
17320
|
return `Message sent to ${args.to}.`;
|
|
17191
17321
|
}
|
|
17322
|
+
if (hasReportedCompletion(deps.db, teamInfo.teamId, args.to)) {
|
|
17323
|
+
return `Message stored for ${args.to} (teammate has completed their task \u2014 message will not wake them).`;
|
|
17324
|
+
}
|
|
17192
17325
|
const deliveryText = `[Team message from ${senderName}]: ${messageText}`;
|
|
17193
17326
|
deps.client.session.promptAsync({
|
|
17194
17327
|
sessionID: recipientSessionId,
|
|
@@ -17223,7 +17356,12 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
17223
17356
|
}
|
|
17224
17357
|
}
|
|
17225
17358
|
let delivered = 0;
|
|
17359
|
+
let skipped = 0;
|
|
17226
17360
|
for (const recipient of recipients) {
|
|
17361
|
+
if (recipient.name !== "lead" && hasReportedCompletion(deps.db, teamInfo.teamId, recipient.name)) {
|
|
17362
|
+
skipped++;
|
|
17363
|
+
continue;
|
|
17364
|
+
}
|
|
17227
17365
|
deps.client.session.promptAsync({
|
|
17228
17366
|
sessionID: recipient.sessionId,
|
|
17229
17367
|
parts: [{ type: "text", text: `[Team broadcast from ${senderName}]: ${args.text}` }]
|
|
@@ -17235,7 +17373,8 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
17235
17373
|
log(`team_broadcast:deliver:failed to=${recipient.name} err=${err instanceof Error ? err.message : String(err)}`);
|
|
17236
17374
|
});
|
|
17237
17375
|
}
|
|
17238
|
-
|
|
17376
|
+
const sent = recipients.length - skipped;
|
|
17377
|
+
return `Broadcast sent to ${sent} recipient${sent !== 1 ? "s" : ""}.`;
|
|
17239
17378
|
}
|
|
17240
17379
|
|
|
17241
17380
|
// src/tools/team-shutdown.ts
|
|
@@ -17930,6 +18069,8 @@ var plugin = async (input) => {
|
|
|
17930
18069
|
const tracker = new DescendantTracker;
|
|
17931
18070
|
const nudgedMembers = new Set;
|
|
17932
18071
|
const progressTracker = new ProgressTracker;
|
|
18072
|
+
const wakeLeadTimestamps = new Map;
|
|
18073
|
+
const WAKE_LEAD_COOLDOWN_MS = 5000;
|
|
17933
18074
|
const pluginTransport = input.client._client;
|
|
17934
18075
|
const rawClient = new OpencodeClient({ client: pluginTransport });
|
|
17935
18076
|
initLog(rawClient);
|
|
@@ -18020,7 +18161,7 @@ var plugin = async (input) => {
|
|
|
18020
18161
|
}
|
|
18021
18162
|
}
|
|
18022
18163
|
const nudgeKey = `${transition.teamId}:${transition.memberName}`;
|
|
18023
|
-
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName)) {
|
|
18164
|
+
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName) && !hasReportedCompletion(db, transition.teamId, transition.memberName)) {
|
|
18024
18165
|
nudgedMembers.add(nudgeKey);
|
|
18025
18166
|
log(`nudge:idle-without-report name=${transition.memberName}`);
|
|
18026
18167
|
client3.session.promptAsync({
|
|
@@ -18063,7 +18204,10 @@ var plugin = async (input) => {
|
|
|
18063
18204
|
const team = db.query("SELECT id FROM team WHERE lead_session_id = ? AND status = 'active'").get(sessionID);
|
|
18064
18205
|
if (team) {
|
|
18065
18206
|
const pending = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = 'lead' AND delivered = 0").get(team.id);
|
|
18066
|
-
|
|
18207
|
+
const allDone = db.query("SELECT COUNT(*) as c FROM team_member WHERE team_id = ? AND status NOT IN ('ready', 'shutdown', 'error')").get(team.id).c === 0;
|
|
18208
|
+
const lastWake = wakeLeadTimestamps.get(team.id) ?? 0;
|
|
18209
|
+
if (pending.c > 0 && !allDone && Date.now() - lastWake > WAKE_LEAD_COOLDOWN_MS) {
|
|
18210
|
+
wakeLeadTimestamps.set(team.id, Date.now());
|
|
18067
18211
|
log(`wake-lead: ${pending.c} pending messages, sending promptAsync`);
|
|
18068
18212
|
client3.session.promptAsync({
|
|
18069
18213
|
sessionID,
|
|
@@ -18076,7 +18220,7 @@ var plugin = async (input) => {
|
|
|
18076
18220
|
const member = db.query(`SELECT tm.team_id, tm.name FROM team_member tm
|
|
18077
18221
|
JOIN team t ON tm.team_id = t.id
|
|
18078
18222
|
WHERE tm.session_id = ? AND t.status = 'active'`).get(sessionID);
|
|
18079
|
-
if (member) {
|
|
18223
|
+
if (member && !hasReportedCompletion(db, member.team_id, member.name)) {
|
|
18080
18224
|
const staleThreshold = Date.now() - 5000;
|
|
18081
18225
|
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);
|
|
18082
18226
|
if (peerMsgs.c > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Agent teams for OpenCode — parallel agents with peer-to-peer communication, shared tasks, and coordinated execution",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"dist",
|
|
24
24
|
"src",
|
|
25
25
|
"LICENSE",
|
|
26
|
-
"README.md"
|
|
26
|
+
"README.md",
|
|
27
|
+
"social-preview.png"
|
|
27
28
|
],
|
|
28
29
|
"scripts": {
|
|
29
30
|
"lint": "bunx biome lint src/",
|
|
Binary file
|
package/src/dashboard-html.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const DASHBOARD_HEAD = `<!DOCTYPE html>
|
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
12
12
|
<script src="https://cdn.tailwindcss.com"><\/script>
|
|
13
|
-
<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:'#
|
|
13
|
+
<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:'#aab4c6',400:'#8a96aa',500:'#7b879b'}},fontFamily:{sans:['Inter','system-ui','sans-serif'],mono:['JetBrains Mono','monospace']}}}}<\/script>
|
|
14
14
|
<style>
|
|
15
15
|
@media(prefers-reduced-motion:no-preference){
|
|
16
16
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
@@ -22,8 +22,10 @@ export const DASHBOARD_HEAD = `<!DOCTYPE html>
|
|
|
22
22
|
.hl{animation:hl 1.5s ease-out}
|
|
23
23
|
.shimmer{background:linear-gradient(90deg,#22c55e 0%,#4ade80 50%,#22c55e 100%)!important;background-size:200% 100%;animation:shimmer 1.5s ease-in-out}
|
|
24
24
|
}
|
|
25
|
+
@media(prefers-reduced-motion:reduce){*,::before,::after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
|
25
26
|
.scroll::-webkit-scrollbar{width:5px}.scroll::-webkit-scrollbar-track{background:transparent}.scroll::-webkit-scrollbar-thumb{background:#2a3144;border-radius:3px}
|
|
26
27
|
details summary::-webkit-details-marker{display:none}details summary{list-style:none}
|
|
28
|
+
:focus-visible{outline:2px solid rgba(96,165,250,.85);outline-offset:2px}
|
|
27
29
|
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}
|
|
28
30
|
.xp{max-height:0;overflow:hidden;transition:max-height .3s ease-out}.xp-open{max-height:3000px;transition:max-height .5s ease-in}
|
|
29
31
|
.card-sel{outline:2px solid rgba(59,130,246,.4);outline-offset:1px}
|
|
@@ -42,13 +44,13 @@ select{-webkit-appearance:none;appearance:none;background-image:url("data:image/
|
|
|
42
44
|
</style>
|
|
43
45
|
</head>
|
|
44
46
|
<body class="bg-base-950 text-txt-100 min-h-screen antialiased font-sans">
|
|
45
|
-
<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">
|
|
46
|
-
<div class="flex items-center gap-3">
|
|
47
|
+
<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-3 sm:px-4 z-50">
|
|
48
|
+
<div class="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
|
47
49
|
<span class="font-mono font-semibold text-[13px] tracking-[.08em] text-txt-200">ensemble</span>
|
|
48
50
|
<span class="text-base-700">|</span>
|
|
49
|
-
<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>
|
|
51
|
+
<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 max-w-[180px] sm:max-w-[320px] min-w-0"></select>
|
|
50
52
|
</div>
|
|
51
|
-
<div class="flex items-center gap-4">
|
|
53
|
+
<div class="flex items-center gap-2 sm:gap-4 shrink-0">
|
|
52
54
|
<div id="hring" class="w-6 h-6 rounded-full" title="Team health"></div>
|
|
53
55
|
<div class="flex items-center gap-2">
|
|
54
56
|
<span id="clk" class="text-[11px] text-txt-400 font-mono"></span>
|
|
@@ -58,25 +60,28 @@ select{-webkit-appearance:none;appearance:none;background-image:url("data:image/
|
|
|
58
60
|
</div>
|
|
59
61
|
</div>
|
|
60
62
|
</header>
|
|
61
|
-
<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>
|
|
63
|
+
<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-3 sm:px-4 gap-3 sm:gap-4 text-[11px] text-txt-300 z-40 overflow-x-auto scroll whitespace-nowrap"></div>
|
|
62
64
|
<main class="pt-[76px] px-4 pb-16">
|
|
63
65
|
<div id="empty" class="hidden flex-col items-center justify-center h-[70vh] gap-3">
|
|
64
66
|
<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>
|
|
65
67
|
<div class="text-txt-400 text-sm">Waiting for a team</div>
|
|
66
68
|
<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>
|
|
67
69
|
</div>
|
|
68
|
-
<div id="content" class="hidden">
|
|
69
|
-
<
|
|
70
|
-
<div class="grid grid-cols-1
|
|
71
|
-
<div id="
|
|
72
|
-
<div
|
|
70
|
+
<div id="content" class="hidden max-w-[1600px] mx-auto">
|
|
71
|
+
<section id="attention" aria-label="Team attention" class="mb-3"></section>
|
|
72
|
+
<div class="grid grid-cols-1 xl:grid-cols-[minmax(360px,1.35fr)_minmax(320px,.8fr)] gap-4 items-start">
|
|
73
|
+
<section aria-label="Agent roster"><div id="agents" class="grid gap-2" style="grid-template-columns:repeat(auto-fill,minmax(300px,1fr))"></div></section>
|
|
74
|
+
<div class="grid grid-cols-1 gap-4">
|
|
75
|
+
<section aria-label="Task board"><div id="tasks" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div></section>
|
|
76
|
+
<section aria-label="Activity feed"><div id="activity" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div></section>
|
|
77
|
+
</div>
|
|
73
78
|
</div>
|
|
74
79
|
</div>
|
|
75
80
|
</main>
|
|
76
|
-
<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>
|
|
77
|
-
<div id="sco" onclick="
|
|
81
|
+
<div id="tl" aria-label="Event timeline" 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>
|
|
82
|
+
<div id="sco" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="shortcuts-title" tabindex="-1" onclick="closeShortcuts()">
|
|
78
83
|
<div class="bg-base-900 border border-base-800 rounded-lg p-6 max-w-sm" onclick="event.stopPropagation()">
|
|
79
|
-
<div class="text-txt-200 font-semibold text-sm mb-4">Keyboard Shortcuts</div>
|
|
84
|
+
<div id="shortcuts-title" class="text-txt-200 font-semibold text-sm mb-4">Keyboard Shortcuts</div>
|
|
80
85
|
<div class="grid grid-cols-2 gap-y-2 gap-x-6 text-[12px]">
|
|
81
86
|
<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>
|
|
82
87
|
<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>
|
|
@@ -88,4 +93,4 @@ select{-webkit-appearance:none;appearance:none;background-image:url("data:image/
|
|
|
88
93
|
</div>
|
|
89
94
|
</div>
|
|
90
95
|
<div id="drawer-bg" onclick="closeDrawer()"></div>
|
|
91
|
-
<div id="drawer" class="scroll p-4"></div>`;
|
|
96
|
+
<div id="drawer" class="scroll p-4" tabindex="-1" inert role="dialog" aria-modal="true" aria-labelledby="drawer-title" aria-hidden="true"><h2 id="drawer-title" class="sr-only">Agent detail</h2></div>`;
|
|
@@ -70,6 +70,40 @@ function deriveHealth(t){
|
|
|
70
70
|
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};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function lastMessageFor(name,msgs){return msgs.filter(m=>m.fromName===name||m.toName===name).sort((a,b)=>b.timeCreated-a.timeCreated)[0]||null}
|
|
74
|
+
function activeTaskFor(name,tasks){return tasks.find(x=>x.assignee===name&&x.status==='in_progress')||tasks.find(x=>x.assignee===name&&x.status==='blocked')||null}
|
|
75
|
+
function blockedTaskFor(name,tasks){return tasks.find(x=>x.assignee===name&&x.status==='blocked')||null}
|
|
76
|
+
|
|
77
|
+
function rankAgent(a,b,t){
|
|
78
|
+
const tasks=t?.tasks||[],msgs=t?.messages||[];
|
|
79
|
+
const score=m=>{
|
|
80
|
+
const lm=lastMessageFor(m.name,msgs),bt=blockedTaskFor(m.name,tasks);
|
|
81
|
+
let s=0;
|
|
82
|
+
if(m.status==='error')s-=1000;
|
|
83
|
+
if(bt)s-=800;
|
|
84
|
+
if(m.status==='shutdown_requested')s-=650;
|
|
85
|
+
if(m.status==='busy')s-=500;
|
|
86
|
+
if(m.status==='ready'&&activeTaskFor(m.name,tasks))s-=350;
|
|
87
|
+
if(lm)s-=Math.max(0,200-Math.floor((Date.now()-lm.timeCreated)/60000));
|
|
88
|
+
if(m.status==='shutdown')s+=700;
|
|
89
|
+
return s;
|
|
90
|
+
};
|
|
91
|
+
const d=score(a)-score(b);return d||String(a.name).localeCompare(String(b.name));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function deriveAttention(t){
|
|
95
|
+
const tasks=t.tasks||[],msgs=t.messages||[];
|
|
96
|
+
const blocked=tasks.filter(x=>x.status==='blocked'),running=tasks.filter(x=>x.status==='in_progress');
|
|
97
|
+
const errored=(t.members||[]).filter(m=>m.status==='error');
|
|
98
|
+
const stopping=(t.members||[]).filter(m=>m.status==='shutdown_requested');
|
|
99
|
+
const latest=msgs[0]||null;
|
|
100
|
+
const items=[];
|
|
101
|
+
errored.forEach(m=>items.push({kind:'Agent error',label:m.name,detail:m.executionStatus||m.status,color:'red'}));
|
|
102
|
+
blocked.forEach(x=>items.push({kind:'Blocked task',label:x.assignee||'unassigned',detail:x.content,color:'amber'}));
|
|
103
|
+
stopping.forEach(m=>items.push({kind:'Stopping',label:m.name,detail:'shutdown requested',color:'amber'}));
|
|
104
|
+
return{items,running,latest,blocked,errored};
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
function deriveSparkline(name,msgs){
|
|
74
108
|
const mine=msgs.filter(m=>m.fromName===name).map(m=>m.timeCreated).sort();
|
|
75
109
|
if(mine.length<2)return '';
|
|
@@ -47,17 +47,32 @@ function rSum(t){
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function rAttention(t){
|
|
51
|
+
const el=document.getElementById('attention'),a=deriveAttention(t),h=deriveHealth(t),tk=t.tasks||[];
|
|
52
|
+
const tone=a.items.length?'border-amber-500/30 bg-amber-500/[0.05]':'border-emerald-500/20 bg-emerald-500/[0.035]';
|
|
53
|
+
const lead=a.items.length?'Needs attention':'No blockers detected';
|
|
54
|
+
const chips=[chip(h.w+' working','blue'),chip(a.running.length+' active tasks','green'),chip(a.blocked.length+' blocked',a.blocked.length?'amber':'muted'),chip(tk.filter(x=>x.status==='pending').length+' pending','muted')];
|
|
55
|
+
let html='<div class="rounded-lg border '+tone+' px-3 py-2 flex flex-col md:flex-row md:items-center gap-2 md:gap-4">';
|
|
56
|
+
html+='<div class="min-w-[180px]"><div class="text-[10px] uppercase tracking-[.16em] text-txt-500">Team attention</div><div class="text-[14px] font-semibold text-txt-100">'+lead+'</div></div>';
|
|
57
|
+
html+='<div class="flex flex-wrap gap-1.5">'+chips.join('')+'</div>';
|
|
58
|
+
if(a.items.length){html+='<div class="flex-1 grid gap-1 md:grid-cols-2">'+a.items.slice(0,4).map(x=>'<div class="text-[12px] text-txt-200 truncate">'+chip(E(x.kind),x.color)+' <span class="font-mono">'+E(x.label)+'</span> <span class="text-txt-400">'+E(x.detail)+'</span></div>').join('')+'</div>'}
|
|
59
|
+
else if(a.latest){html+='<div class="text-[12px] text-txt-400 truncate md:ml-auto">Latest: <span class="text-txt-200">'+E(a.latest.fromName)+'</span> '+relT(a.latest.timeCreated)+'</div>'}
|
|
60
|
+
html+='</div>';
|
|
61
|
+
patch(el,html);
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
function rAgents(t){
|
|
51
65
|
const el=document.getElementById('agents'),mm=t.members||[];
|
|
52
66
|
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}
|
|
53
|
-
const n=Date.now(),msgs=t.messages||[],sorted=[...mm].sort((a,b)=>
|
|
67
|
+
const n=Date.now(),msgs=t.messages||[],sorted=[...mm].sort((a,b)=>rankAgent(a,b,t));
|
|
54
68
|
const html=sorted.map((m,idx)=>{
|
|
55
|
-
const s=si(m.status),task=(t.tasks||[])
|
|
69
|
+
const s=si(m.status),task=activeTaskFor(m.name,t.tasks||[]),msg=lastMessageFor(m.name,msgs),blocked=blockedTaskFor(m.name,t.tasks||[]);
|
|
56
70
|
const d=D(n-m.timeUpdated),mi=msg?relT(msg.timeCreated):'\\u2014';
|
|
57
71
|
const tt=task?.content,tr=tt&&tt.length>90?tt.slice(0,90)+'\\u2026':tt;
|
|
72
|
+
const mp=msg?(msg.content.length>90?msg.content.slice(0,90)+'\\u2026':msg.content):'';
|
|
58
73
|
const spark=deriveSparkline(m.name,msgs);
|
|
59
74
|
const isSel=selCard===idx;
|
|
60
|
-
return '<
|
|
75
|
+
return '<button type="button" class="text-left rounded-lg border '+s.c+' p-3 transition-all duration-300 cursor-pointer hover:border-base-600 focus-visible:border-blue-400'+(isSel?' card-sel':'')+'" data-card="'+E(m.name)+'" onclick="openDrawer(this.dataset.card)" onkeydown="if(event.key===\\'Enter\\'||event.key===\\' \\'){event.preventDefault();openDrawer(this.dataset.card)}">'+
|
|
61
76
|
'<div class="flex items-center gap-2">'+
|
|
62
77
|
'<span class="w-[8px] h-[8px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+' shrink-0"></span>'+
|
|
63
78
|
'<span class="font-mono font-semibold text-[14px] truncate">'+E(m.name)+'</span>'+
|
|
@@ -66,11 +81,12 @@ function rAgents(t){
|
|
|
66
81
|
'<span class="text-[10px] text-txt-500 ml-auto shrink-0">'+E(m.agent)+'</span>'+
|
|
67
82
|
(m.model?chip(E(m.model),'muted'):'')+
|
|
68
83
|
'</div>'+
|
|
69
|
-
(tr?'<div class="mt-2 text-[13px] text-txt-200 leading-snug truncate">'+E(tr)+'</div>':'')+
|
|
84
|
+
(tr?'<div class="mt-2 text-[13px] text-txt-200 leading-snug truncate">'+(blocked?'<span class="text-amber-400">Blocked: </span>':'Current task: ')+E(tr)+'</div>':'<div class="mt-2 text-[13px] text-txt-500 leading-snug">No active task</div>')+
|
|
85
|
+
(mp?'<div class="mt-1 text-[12px] text-txt-400 truncate">Latest: '+E(mp)+'</div>':'')+
|
|
70
86
|
'<div class="mt-2 flex items-center gap-1.5 flex-wrap">'+
|
|
71
|
-
chip(d,'muted')+chip('msg '+mi,'muted')+
|
|
87
|
+
chip('status '+d,'muted')+chip('msg '+mi,'muted')+chip(E(m.executionStatus||m.status),m.status==='busy'?'blue':m.status==='error'?'red':'muted')+
|
|
72
88
|
(m.worktreeBranch?chip(E(m.worktreeBranch),'muted'):'')+
|
|
73
|
-
'</div></
|
|
89
|
+
'</div></button>';
|
|
74
90
|
}).join('');
|
|
75
91
|
patch(el,html);
|
|
76
92
|
}
|
|
@@ -82,8 +98,8 @@ function openDrawer(name){
|
|
|
82
98
|
var h='';
|
|
83
99
|
// Header
|
|
84
100
|
h+='<div class="flex items-center justify-between mb-4">';
|
|
85
|
-
h+='<div class="flex items-center gap-2"><span class="w-[10px] h-[10px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+'"></span><
|
|
86
|
-
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>';
|
|
101
|
+
h+='<div class="flex items-center gap-2"><span class="w-[10px] h-[10px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+'"></span><h2 id="drawer-title" class="font-mono font-semibold text-[16px]">'+E(m.name)+'</h2><span class="text-[11px] px-2 py-[2px] rounded '+s.t+' bg-base-800/80">'+s.l+'</span></div>';
|
|
102
|
+
h+='<button id="drawer-close" aria-label="Close agent detail" 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>';
|
|
87
103
|
h+='</div>';
|
|
88
104
|
// Meta chips
|
|
89
105
|
var meta=[];
|
|
@@ -130,13 +146,22 @@ function openDrawer(name){
|
|
|
130
146
|
h+='</div></div>';
|
|
131
147
|
});
|
|
132
148
|
}
|
|
133
|
-
document.getElementById('drawer')
|
|
134
|
-
|
|
149
|
+
var drawer=document.getElementById('drawer');
|
|
150
|
+
drawer.innerHTML=h;
|
|
151
|
+
drawer.classList.add('open');
|
|
152
|
+
drawer.setAttribute('aria-hidden','false');
|
|
153
|
+
drawer.inert=false;
|
|
154
|
+
setBackgroundInert(true);
|
|
155
|
+
drawer.focus();
|
|
135
156
|
document.getElementById('drawer-bg').classList.add('open');
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
function closeDrawer(){
|
|
139
|
-
document.getElementById('drawer')
|
|
160
|
+
var drawer=document.getElementById('drawer');
|
|
161
|
+
drawer.classList.remove('open');
|
|
162
|
+
drawer.setAttribute('aria-hidden','true');
|
|
163
|
+
drawer.inert=true;
|
|
164
|
+
if(!modalOpen())setBackgroundInert(false);
|
|
140
165
|
document.getElementById('drawer-bg').classList.remove('open');
|
|
141
166
|
}
|
|
142
167
|
|
|
@@ -155,7 +180,8 @@ function rTasks(t){
|
|
|
155
180
|
const icl=x.status==='completed'?'text-emerald-500':x.status==='in_progress'?'text-blue-400':'text-txt-500';
|
|
156
181
|
const p=PR[x.priority]||PR.low,cn=x.content.length>55?x.content.slice(0,55)+'\\u2026':x.content;
|
|
157
182
|
let dep='';
|
|
158
|
-
|
|
183
|
+
const depIds=Array.isArray(x.dependsOn)?x.dependsOn:(x.dependsOn?[String(x.dependsOn)]:[]);
|
|
184
|
+
if(depIds.length){const labels=depIds.map(id=>{const parent=taskMap[id];return parent?parent.content.slice(0,42):id}).join(', ');dep='<div class="text-[10px] text-txt-500 mt-0.5">blocked by: '+E(labels)+'</div>'}
|
|
159
185
|
return '<div class="flex items-start gap-2 py-1.5 px-1 rounded hover:bg-base-800/40 transition-colors">'+
|
|
160
186
|
'<span class="'+icl+' text-[11px] mt-[2px] shrink-0">'+ic+'</span>'+
|
|
161
187
|
'<div class="flex-1 min-w-0"><div class="text-[13px] text-txt-200 truncate">'+E(cn)+'</div>'+
|
|
@@ -182,7 +208,7 @@ function rActivity(t){
|
|
|
182
208
|
if(!msgs.length){patch(el,'<div class="text-txt-500 text-sm py-6 text-center">Waiting for agent messages...</div>');return}
|
|
183
209
|
const sp=saveSc(el);
|
|
184
210
|
const newCount=msgs.length,hasNew=newCount>prevMC&&prevMC>0;
|
|
185
|
-
let html='<div class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider
|
|
211
|
+
let html='<div class="flex items-center gap-2 mb-3"><span class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider">Activity feed</span>'+chip(msgs.length+' msgs','muted')+'</div>';
|
|
186
212
|
html+='<div class="max-h-[50vh] overflow-y-auto scroll space-y-2">';
|
|
187
213
|
msgs.slice(0,30).forEach(function(m,mi){
|
|
188
214
|
const isNew=hasNew&&mi<(newCount-prevMC);
|
|
@@ -195,7 +221,7 @@ function rActivity(t){
|
|
|
195
221
|
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';
|
|
196
222
|
const initial=m.fromName.charAt(0).toUpperCase();
|
|
197
223
|
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';
|
|
198
|
-
html+='<div class="'+align+(isNew?' hl':'')+' cursor-pointer" data-msg="'+E(m.id)+'" onclick="toggleMsg(\\'
|
|
224
|
+
html+='<div class="'+align+(isNew?' hl':'')+' cursor-pointer" role="button" tabindex="0" aria-expanded="'+(isExp?'true':'false')+'" data-msg="'+E(m.id)+'" onclick="toggleMsg(this.dataset.msg)" onkeydown="if(event.key===\\'Enter\\'||event.key===\\' \\'){event.preventDefault();toggleMsg(this.dataset.msg)}">';
|
|
199
225
|
html+='<div class="flex items-start gap-2">';
|
|
200
226
|
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>';
|
|
201
227
|
html+='<div class="flex-1 min-w-0 rounded-xl border '+bubbleBg+' px-3 py-2">';
|
|
@@ -7,16 +7,49 @@ function render(){
|
|
|
7
7
|
const empty=document.getElementById('empty'),content=document.getElementById('content');
|
|
8
8
|
if(!t){empty.classList.remove('hidden');empty.classList.add('flex');content.classList.add('hidden');document.getElementById('tl').classList.add('hidden');return}
|
|
9
9
|
empty.classList.add('hidden');empty.classList.remove('flex');content.classList.remove('hidden');
|
|
10
|
-
rHealth(t);rSum(t);rAgents(t);rTasks(t);rActivity(t);rTimeline(t);
|
|
10
|
+
rHealth(t);rSum(t);rAttention(t);rAgents(t);rTasks(t);rActivity(t);rTimeline(t);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function conn(ok){
|
|
14
14
|
document.getElementById('cd').className='w-[7px] h-[7px] rounded-full '+(ok?'bg-emerald-500 pulse':'bg-red-500');
|
|
15
|
-
document.getElementById('ct').textContent=ok?D(Date.now()-pollT)+' ago':'
|
|
15
|
+
document.getElementById('ct').textContent=ok?D(Date.now()-pollT)+' ago':'reconnecting to dashboard state';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
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)}}
|
|
19
19
|
|
|
20
|
+
function setBackgroundInert(locked){
|
|
21
|
+
document.querySelectorAll('header,main,#sum,#tl').forEach(function(el){
|
|
22
|
+
el.inert=locked;
|
|
23
|
+
if(locked)el.setAttribute('aria-hidden','true');
|
|
24
|
+
else el.removeAttribute('aria-hidden');
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function modalOpen(){return document.getElementById('sco').classList.contains('show')||document.getElementById('drawer').classList.contains('open')}
|
|
29
|
+
|
|
30
|
+
function trapFocus(root,e){
|
|
31
|
+
const nodes=[...root.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])')].filter(function(el){return !el.disabled&&!el.inert&&el.offsetParent!==null});
|
|
32
|
+
if(!nodes.length){e.preventDefault();root.focus();return}
|
|
33
|
+
const first=nodes[0],last=nodes[nodes.length-1];
|
|
34
|
+
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();return}
|
|
35
|
+
if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();return}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function openShortcuts(){
|
|
39
|
+
const el=document.getElementById('sco');
|
|
40
|
+
setBackgroundInert(true);
|
|
41
|
+
el.classList.add('show');
|
|
42
|
+
el.setAttribute('aria-hidden','false');
|
|
43
|
+
document.getElementById('sco').focus();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function closeShortcuts(){
|
|
47
|
+
const el=document.getElementById('sco');
|
|
48
|
+
el.classList.remove('show');
|
|
49
|
+
el.setAttribute('aria-hidden','true');
|
|
50
|
+
if(!modalOpen())setBackgroundInert(false);
|
|
51
|
+
}
|
|
52
|
+
|
|
20
53
|
// Clock update every second
|
|
21
54
|
setInterval(function(){var t=cur();if(t)rClock(t);if(fails<3)conn(true)},1000);
|
|
22
55
|
|
|
@@ -25,11 +58,18 @@ setInterval(poll,2500);
|
|
|
25
58
|
|
|
26
59
|
// Keyboard shortcuts
|
|
27
60
|
document.addEventListener('keydown',function(e){
|
|
61
|
+
const shortcutsOpen=document.getElementById('sco').classList.contains('show');
|
|
62
|
+
if(shortcutsOpen&&e.key==='Tab'){trapFocus(document.getElementById('sco'),e);return}
|
|
63
|
+
if(shortcutsOpen&&e.key==='Escape'){e.preventDefault();closeShortcuts();return}
|
|
64
|
+
const drawerOpen=document.getElementById('drawer').classList.contains('open');
|
|
65
|
+
if(drawerOpen&&e.key==='Tab'){trapFocus(document.getElementById('drawer'),e);return}
|
|
66
|
+
if(drawerOpen&&e.key==='Escape'){e.preventDefault();closeDrawer();return}
|
|
67
|
+
if(drawerOpen&&e.key==='?'){e.preventDefault();return}
|
|
28
68
|
if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT'||e.target.tagName==='TEXTAREA')return;
|
|
29
69
|
var t=cur();if(!t)return;
|
|
30
|
-
var mm=t.members||[];
|
|
31
|
-
if(e.key==='?'){e.preventDefault();
|
|
32
|
-
if(e.key==='Escape'){closeDrawer();expMsgs.clear();selCard=-1;
|
|
70
|
+
var mm=[...(t.members||[])].sort((a,b)=>rankAgent(a,b,t));
|
|
71
|
+
if(e.key==='?'){e.preventDefault();shortcutsOpen?closeShortcuts():openShortcuts();return}
|
|
72
|
+
if(e.key==='Escape'){closeDrawer();expMsgs.clear();selCard=-1;closeShortcuts();render();return}
|
|
33
73
|
if(e.key==='j'&&mm.length){e.preventDefault();selCard=Math.min(selCard+1,mm.length-1);render();return}
|
|
34
74
|
if(e.key==='k'&&mm.length){e.preventDefault();selCard=Math.max(selCard-1,0);render();return}
|
|
35
75
|
if(e.key==='Enter'&&selCard>=0&&selCard<mm.length){e.preventDefault();openDrawer(mm[selCard].name);return}
|
package/src/dashboard.ts
CHANGED
|
@@ -58,6 +58,21 @@ function jsonResponse(data: unknown, status = 200): Response {
|
|
|
58
58
|
})
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function parseDependsOn(value: string | null): string[] {
|
|
62
|
+
if (!value) return []
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const parsed: unknown = JSON.parse(value)
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string")
|
|
68
|
+
if (typeof parsed === "string") return [parsed]
|
|
69
|
+
} catch {
|
|
70
|
+
return [value]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
function buildState(db: Database): { teams: unknown[] } {
|
|
62
77
|
const teams = db.query("SELECT id, name, status, lead_agent, time_created, time_updated FROM team ORDER BY time_created DESC").all() as TeamRow[]
|
|
63
78
|
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 = ?")
|
|
@@ -90,7 +105,7 @@ function buildState(db: Database): { teams: unknown[] } {
|
|
|
90
105
|
status: tk.status,
|
|
91
106
|
priority: tk.priority,
|
|
92
107
|
assignee: tk.assignee,
|
|
93
|
-
dependsOn: tk.depends_on,
|
|
108
|
+
dependsOn: parseDependsOn(tk.depends_on),
|
|
94
109
|
timeCreated: tk.time_created,
|
|
95
110
|
timeUpdated: tk.time_updated,
|
|
96
111
|
})),
|
package/src/hooks.ts
CHANGED
|
@@ -41,11 +41,26 @@ export function handleSessionStatusEvent(
|
|
|
41
41
|
"UPDATE team_member SET status = ?, execution_status = 'idle', time_updated = ? WHERE team_id = ? AND name = ?",
|
|
42
42
|
[newStatus, Date.now(), entry.teamId, entry.memberName]
|
|
43
43
|
)
|
|
44
|
+
// Mark teammate as having reported if they sent at least one message to lead (issue #3).
|
|
45
|
+
// Set on busy→ready transition so Q&A messages during work don't prematurely block delivery.
|
|
46
|
+
if (member.status === "busy" && newStatus === "ready") {
|
|
47
|
+
const leadMsgCount = (db.query(
|
|
48
|
+
"SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND from_name = ? AND to_name = 'lead'"
|
|
49
|
+
).get(entry.teamId, entry.memberName) as { c: number }).c
|
|
50
|
+
if (leadMsgCount > 0) {
|
|
51
|
+
db.run(
|
|
52
|
+
"UPDATE team_member SET reported_to_lead = 1 WHERE team_id = ? AND name = ?",
|
|
53
|
+
[entry.teamId, entry.memberName]
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
44
57
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: newStatus }
|
|
45
58
|
} else if (status === "busy") {
|
|
46
59
|
if (member.status === "ready" || member.status === "error") {
|
|
60
|
+
// Reset reported_to_lead so re-activated teammates can receive messages again (issue #3).
|
|
61
|
+
// INVARIANT: every promptAsync delivery path must check hasReportedCompletion() to prevent loops.
|
|
47
62
|
db.run(
|
|
48
|
-
"UPDATE team_member SET status = 'busy', execution_status = 'running', time_updated = ? WHERE team_id = ? AND name = ?",
|
|
63
|
+
"UPDATE team_member SET status = 'busy', execution_status = 'running', reported_to_lead = 0, time_updated = ? WHERE team_id = ? AND name = ?",
|
|
49
64
|
[Date.now(), entry.teamId, entry.memberName]
|
|
50
65
|
)
|
|
51
66
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: "busy" }
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { MemberRegistry, DescendantTracker } from "./state"
|
|
|
10
10
|
import { isWorktreeInstance } from "./util"
|
|
11
11
|
import { handleSessionStatusEvent, handleSessionCreatedEvent, checkToolIsolation, shouldNudgeIdleMember } from "./hooks"
|
|
12
12
|
import { notifyTeamEvent, notifyWorkingProgress } from "./notify"
|
|
13
|
-
import { sendMessage } from "./messaging"
|
|
13
|
+
import { sendMessage, hasReportedCompletion } from "./messaging"
|
|
14
14
|
import { buildLeadSystemPrompt, buildTeammateSystemPrompt, buildTeamCompactionContext } from "./system-prompt"
|
|
15
15
|
import { log, initLog } from "./log"
|
|
16
16
|
import { findTeamBySession } from "./types"
|
|
@@ -58,6 +58,8 @@ const plugin: Plugin = async (input) => {
|
|
|
58
58
|
const tracker = new DescendantTracker()
|
|
59
59
|
const nudgedMembers = new Set<string>()
|
|
60
60
|
const progressTracker = new ProgressTracker()
|
|
61
|
+
const wakeLeadTimestamps = new Map<string, number>()
|
|
62
|
+
const WAKE_LEAD_COOLDOWN_MS = 5000
|
|
61
63
|
|
|
62
64
|
// Extract the working HeyAPI transport from the plugin-provided v1 client and pass it
|
|
63
65
|
// to the v2 OpencodeClient. The plugin framework provides a v1 client which stores its
|
|
@@ -176,8 +178,9 @@ const plugin: Plugin = async (input) => {
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
// Nudge teammate if they went idle without reporting to the lead (once only)
|
|
181
|
+
// Skip if they already reported completion (issue #3 — prevents re-waking completed teammates)
|
|
179
182
|
const nudgeKey = `${transition.teamId}:${transition.memberName}`
|
|
180
|
-
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName)) {
|
|
183
|
+
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName) && !hasReportedCompletion(db, transition.teamId, transition.memberName)) {
|
|
181
184
|
nudgedMembers.add(nudgeKey)
|
|
182
185
|
log(`nudge:idle-without-report name=${transition.memberName}`)
|
|
183
186
|
client.session.promptAsync({
|
|
@@ -232,7 +235,13 @@ const plugin: Plugin = async (input) => {
|
|
|
232
235
|
const team = db.query("SELECT id FROM team WHERE lead_session_id = ? AND status = 'active'").get(sessionID) as { id: string } | null
|
|
233
236
|
if (team) {
|
|
234
237
|
const pending = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = 'lead' AND delivered = 0").get(team.id) as { c: number }
|
|
235
|
-
if (
|
|
238
|
+
// Skip wake if all teammates are done or if we woke recently (issue #3 — breaks completion loop)
|
|
239
|
+
const allDone = (db.query(
|
|
240
|
+
"SELECT COUNT(*) as c FROM team_member WHERE team_id = ? AND status NOT IN ('ready', 'shutdown', 'error')"
|
|
241
|
+
).get(team.id) as { c: number }).c === 0
|
|
242
|
+
const lastWake = wakeLeadTimestamps.get(team.id) ?? 0
|
|
243
|
+
if (pending.c > 0 && !allDone && Date.now() - lastWake > WAKE_LEAD_COOLDOWN_MS) {
|
|
244
|
+
wakeLeadTimestamps.set(team.id, Date.now())
|
|
236
245
|
log(`wake-lead: ${pending.c} pending messages, sending promptAsync`)
|
|
237
246
|
client.session.promptAsync({
|
|
238
247
|
sessionID,
|
|
@@ -250,7 +259,7 @@ const plugin: Plugin = async (input) => {
|
|
|
250
259
|
JOIN team t ON tm.team_id = t.id
|
|
251
260
|
WHERE tm.session_id = ? AND t.status = 'active'`
|
|
252
261
|
).get(sessionID) as { team_id: string; name: string } | null
|
|
253
|
-
if (member) {
|
|
262
|
+
if (member && !hasReportedCompletion(db, member.team_id, member.name)) {
|
|
254
263
|
const staleThreshold = Date.now() - 5000
|
|
255
264
|
const peerMsgs = db.query(
|
|
256
265
|
"SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = ? AND delivered = 0 AND time_created < ?"
|
package/src/messaging.ts
CHANGED
|
@@ -76,3 +76,14 @@ export function getUndeliveredMessages(db: Database, teamId: string): MessageRow
|
|
|
76
76
|
export function markDelivered(db: Database, messageId: string): void {
|
|
77
77
|
db.run("UPDATE team_message SET delivered = 1 WHERE id = ?", [messageId])
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a teammate has reported completion to the lead.
|
|
82
|
+
* Returns true if the reported_to_lead flag is set on the member.
|
|
83
|
+
*/
|
|
84
|
+
export function hasReportedCompletion(db: Database, teamId: string, memberName: string): boolean {
|
|
85
|
+
const row = db.query(
|
|
86
|
+
"SELECT reported_to_lead FROM team_member WHERE team_id = ? AND name = ?"
|
|
87
|
+
).get(teamId, memberName) as { reported_to_lead: number } | null
|
|
88
|
+
return row?.reported_to_lead === 1
|
|
89
|
+
}
|
package/src/recovery.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import type { PluginClient } from "./types"
|
|
3
3
|
import type { MemberRegistry } from "./state"
|
|
4
|
-
import { getUndeliveredMessages, markDelivered } from "./messaging"
|
|
4
|
+
import { getUndeliveredMessages, markDelivered, hasReportedCompletion } from "./messaging"
|
|
5
5
|
import { preserveBranch, preservedBranchName } from "./tools/merge-helper"
|
|
6
6
|
import { log } from "./log"
|
|
7
7
|
|
|
@@ -123,6 +123,12 @@ export async function recoverUndeliveredMessages(
|
|
|
123
123
|
|
|
124
124
|
if (!recipientSessionId) continue
|
|
125
125
|
|
|
126
|
+
// Skip delivery to teammates who have already reported completion (issue #3)
|
|
127
|
+
if (hasReportedCompletion(db, team.id, msg.to_name!)) {
|
|
128
|
+
markDelivered(db, msg.id)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
try {
|
|
127
133
|
await client.session.promptAsync({
|
|
128
134
|
sessionID: recipientSessionId,
|
package/src/schema.ts
CHANGED
|
@@ -80,6 +80,8 @@ export const MIGRATIONS: string[] = [
|
|
|
80
80
|
`ALTER TABLE team ADD COLUMN lead_agent TEXT;`,
|
|
81
81
|
// Migration 6: Track workspace ID for worktree-session binding
|
|
82
82
|
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT;`,
|
|
83
|
+
// Migration 7: Track whether teammate has reported to lead (completion loop prevention, issue #3)
|
|
84
|
+
`ALTER TABLE team_member ADD COLUMN reported_to_lead INTEGER NOT NULL DEFAULT 0;`,
|
|
83
85
|
]
|
|
84
86
|
|
|
85
87
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolDeps } from "../types"
|
|
2
2
|
import { requireTeamMember } from "./shared"
|
|
3
|
-
import { broadcastMessage, markDelivered } from "../messaging"
|
|
3
|
+
import { broadcastMessage, markDelivered, hasReportedCompletion } from "../messaging"
|
|
4
4
|
import { log } from "../log"
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -40,8 +40,14 @@ export async function executeTeamBroadcast(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Fire-and-forget: deliver to all recipients. Message is already persisted in DB.
|
|
43
|
+
// Skip completed teammates to prevent re-waking them (issue #3).
|
|
43
44
|
let delivered = 0
|
|
45
|
+
let skipped = 0
|
|
44
46
|
for (const recipient of recipients) {
|
|
47
|
+
if (recipient.name !== "lead" && hasReportedCompletion(deps.db, teamInfo.teamId, recipient.name)) {
|
|
48
|
+
skipped++
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
45
51
|
deps.client.session.promptAsync({
|
|
46
52
|
sessionID: recipient.sessionId,
|
|
47
53
|
parts: [{ type: "text", text: `[Team broadcast from ${senderName}]: ${args.text}` }],
|
|
@@ -53,5 +59,6 @@ export async function executeTeamBroadcast(
|
|
|
53
59
|
})
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
const sent = recipients.length - skipped
|
|
63
|
+
return `Broadcast sent to ${sent} recipient${sent !== 1 ? "s" : ""}.`
|
|
57
64
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ToolDeps } from "../types"
|
|
2
2
|
import { resolveRecipientSession } from "../types"
|
|
3
3
|
import { requireTeamMember } from "./shared"
|
|
4
|
-
import { sendMessage, markDelivered } from "../messaging"
|
|
4
|
+
import { sendMessage, markDelivered, hasReportedCompletion } from "../messaging"
|
|
5
5
|
import { log } from "../log"
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -90,6 +90,11 @@ export async function executeTeamMessage(
|
|
|
90
90
|
return `Message sent to ${args.to}.`
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// Guard: skip promptAsync delivery to teammates who have already reported completion (issue #3)
|
|
94
|
+
if (hasReportedCompletion(deps.db, teamInfo.teamId, args.to)) {
|
|
95
|
+
return `Message stored for ${args.to} (teammate has completed their task — message will not wake them).`
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
// For member-to-member messages, fire-and-forget delivery is safe.
|
|
94
99
|
const deliveryText = `[Team message from ${senderName}]: ${messageText}`
|
|
95
100
|
deps.client.session.promptAsync({
|