herdr-plugin-amq 0.1.8 → 0.1.9
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 +35 -268
- package/docs/architecture.md +50 -0
- package/docs/cli-and-workflows.md +72 -0
- package/docs/images/agmail-agent-activity.webp +0 -0
- package/docs/images/agmail-mobile-new-task.webp +0 -0
- package/docs/images/agmail-task-drawer.webp +0 -0
- package/docs/installation.md +70 -0
- package/docs/security-and-testing.md +33 -0
- package/docs/ui-screenshots.md +21 -0
- package/package.json +11 -5
- package/skills/herdr-amq/SKILL.md +4 -3
- package/src/actions.mjs +2 -2
- package/src/bridge.mjs +159 -53
- package/src/fleet.mjs +12 -10
- package/src/herdr.mjs +68 -14
- package/src/index.mjs +1 -0
- package/src/protocol.mjs +315 -67
- package/src/runtime-models.mjs +118 -0
- package/src/server.mjs +81 -37
- package/src/store.mjs +219 -34
- package/src/templates.mjs +118 -0
- package/src/web/app.js +487 -105
- package/src/web/index.html +130 -11
- package/src/web/style.css +1022 -39
package/src/web/app.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
(function () {
|
|
3
3
|
const state = {
|
|
4
4
|
activeAccount: "all",
|
|
5
|
-
activePersona:
|
|
5
|
+
activePersona: "user",
|
|
6
6
|
activeFolder: "inbox",
|
|
7
7
|
activeCategory: "all",
|
|
8
8
|
viewMode: "threads", // "threads" (Conversation view) or "flat" (Individual transmissions)
|
|
@@ -21,15 +21,26 @@
|
|
|
21
21
|
status: {},
|
|
22
22
|
contextTarget: null,
|
|
23
23
|
currentView: "mail", // "mail" or "board"
|
|
24
|
+
readingLayout: document.documentElement.dataset.readingLayout === "full" ? "full" : "split",
|
|
25
|
+
themePreference: ["system", "light", "dark"].includes(document.documentElement.dataset.themePreference) ? document.documentElement.dataset.themePreference : "system",
|
|
26
|
+
detailOpen: false,
|
|
24
27
|
board: null,
|
|
25
28
|
boardFilterAgent: "all",
|
|
26
29
|
boardSearchQuery: "",
|
|
27
30
|
selectedTaskId: null,
|
|
31
|
+
activeAgentHandle: null,
|
|
28
32
|
};
|
|
29
33
|
|
|
34
|
+
function getActiveSender(selected = "") {
|
|
35
|
+
if (state.activeAccount === "all") return "user";
|
|
36
|
+
return selected || state.activeAccount || state.activePersona || "user";
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
// DOM Elements
|
|
31
40
|
const mailListEl = document.getElementById("mail-list");
|
|
41
|
+
const mailListViewEl = document.getElementById("mail-list-view");
|
|
32
42
|
const mailDetailViewEl = document.getElementById("mail-detail-view");
|
|
43
|
+
const contentSplitterEl = document.getElementById("splitter-view");
|
|
33
44
|
const searchInputEl = document.getElementById("search-input");
|
|
34
45
|
const clearSearchBtn = document.getElementById("clear-search");
|
|
35
46
|
const headerSearchContainer = document.getElementById("header-search-container");
|
|
@@ -52,6 +63,14 @@
|
|
|
52
63
|
const sidebarBackdrop = document.getElementById("sidebar-backdrop");
|
|
53
64
|
const sidebarEl = document.getElementById("sidebar");
|
|
54
65
|
|
|
66
|
+
const openSettingsBtn = document.getElementById("open-settings-btn");
|
|
67
|
+
const settingsBackdrop = document.getElementById("settings-backdrop");
|
|
68
|
+
const settingsForm = document.getElementById("settings-form");
|
|
69
|
+
const closeSettingsBtn = document.getElementById("close-settings-btn");
|
|
70
|
+
const cancelSettingsBtn = document.getElementById("cancel-settings-btn");
|
|
71
|
+
const readingLayoutInputs = [...document.querySelectorAll('input[name="reading-layout"]')];
|
|
72
|
+
const themeInputs = [...document.querySelectorAll('input[name="theme"]')];
|
|
73
|
+
|
|
55
74
|
// Account Menu Elements
|
|
56
75
|
const userProfileBtn = document.getElementById("user-profile-btn");
|
|
57
76
|
const accountDropdown = document.getElementById("account-dropdown");
|
|
@@ -60,8 +79,26 @@
|
|
|
60
79
|
const currentUserAvatar = document.getElementById("current-user-avatar");
|
|
61
80
|
const dropdownLargeAvatar = document.getElementById("dropdown-large-avatar");
|
|
62
81
|
const dropdownUserTitle = document.getElementById("dropdown-user-title");
|
|
82
|
+
const dropdownUserRole = document.getElementById("dropdown-user-role");
|
|
63
83
|
const dropdownUserEmail = document.getElementById("dropdown-user-email");
|
|
64
84
|
|
|
85
|
+
const agentActivityDialog = document.getElementById("agent-activity-dialog");
|
|
86
|
+
const closeAgentActivityBtn = document.getElementById("close-agent-activity-btn");
|
|
87
|
+
const agentActivityAvatar = document.getElementById("agent-activity-avatar");
|
|
88
|
+
const agentActivityName = document.getElementById("agent-activity-name");
|
|
89
|
+
const agentActivityHandle = document.getElementById("agent-activity-handle");
|
|
90
|
+
const agentActivityRole = document.getElementById("agent-activity-role");
|
|
91
|
+
const agentActivityStatus = document.getElementById("agent-activity-status");
|
|
92
|
+
const agentActivityHeadline = document.getElementById("agent-activity-headline");
|
|
93
|
+
const agentActivityTask = document.getElementById("agent-activity-task");
|
|
94
|
+
const agentActivityTaskStatus = document.getElementById("agent-activity-task-status");
|
|
95
|
+
const agentActivityModel = document.getElementById("agent-activity-model");
|
|
96
|
+
const agentActivityUnread = document.getElementById("agent-activity-unread");
|
|
97
|
+
const agentActivityPane = document.getElementById("agent-activity-pane");
|
|
98
|
+
const agentActivityObserved = document.getElementById("agent-activity-observed");
|
|
99
|
+
const viewAgentInboxBtn = document.getElementById("view-agent-inbox-btn");
|
|
100
|
+
const viewAgentTaskBtn = document.getElementById("view-agent-task-btn");
|
|
101
|
+
|
|
65
102
|
// Context Menu Elements
|
|
66
103
|
const chatContextMenu = document.getElementById("chat-context-menu");
|
|
67
104
|
const ctxHeader = document.getElementById("ctx-header");
|
|
@@ -159,6 +196,7 @@
|
|
|
159
196
|
const newTaskForm = document.getElementById("new-task-form");
|
|
160
197
|
const taskTitleInput = document.getElementById("task-title-input");
|
|
161
198
|
const taskOwnerSelect = document.getElementById("task-owner-select");
|
|
199
|
+
const taskOwnerTip = document.getElementById("task-owner-tip");
|
|
162
200
|
const taskStatusSelect = document.getElementById("task-status-select");
|
|
163
201
|
const taskDescInput = document.getElementById("task-desc-input");
|
|
164
202
|
|
|
@@ -195,6 +233,95 @@
|
|
|
195
233
|
const sheetDispatchThreadPreview = document.getElementById("sheet-dispatch-thread-preview");
|
|
196
234
|
const sheetDispatchSendBtn = document.getElementById("sheet-dispatch-send-btn");
|
|
197
235
|
|
|
236
|
+
const systemThemeMedia = window.matchMedia("(prefers-color-scheme: dark)");
|
|
237
|
+
let settingsReturnFocus = null;
|
|
238
|
+
|
|
239
|
+
function persistPreference(key, value) {
|
|
240
|
+
try {
|
|
241
|
+
localStorage.setItem(key, value);
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveTheme(preference) {
|
|
246
|
+
return preference === "dark" || (preference === "system" && systemThemeMedia.matches) ? "dark" : "light";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function applyThemePreference(preference, persist = true) {
|
|
250
|
+
const normalized = ["system", "light", "dark"].includes(preference) ? preference : "system";
|
|
251
|
+
state.themePreference = normalized;
|
|
252
|
+
document.documentElement.dataset.themePreference = normalized;
|
|
253
|
+
document.documentElement.dataset.theme = resolveTheme(normalized);
|
|
254
|
+
if (persist) persistPreference("agmail_theme", normalized);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function applyReadingLayout(layout, persist = true) {
|
|
258
|
+
const normalized = layout === "full" ? "full" : "split";
|
|
259
|
+
state.readingLayout = normalized;
|
|
260
|
+
document.documentElement.dataset.readingLayout = normalized;
|
|
261
|
+
contentSplitterEl.dataset.readingLayout = normalized;
|
|
262
|
+
if (persist) persistPreference("agmail_reading_layout", normalized);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function syncSettingsForm() {
|
|
266
|
+
readingLayoutInputs.forEach((input) => {
|
|
267
|
+
input.checked = input.value === state.readingLayout;
|
|
268
|
+
});
|
|
269
|
+
themeInputs.forEach((input) => {
|
|
270
|
+
input.checked = input.value === state.themePreference;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function openSettings() {
|
|
275
|
+
settingsReturnFocus = document.activeElement;
|
|
276
|
+
syncSettingsForm();
|
|
277
|
+
settingsBackdrop.classList.remove("hidden");
|
|
278
|
+
requestAnimationFrame(() => closeSettingsBtn.focus());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function closeSettings() {
|
|
282
|
+
if (settingsBackdrop.classList.contains("hidden")) return;
|
|
283
|
+
settingsBackdrop.classList.add("hidden");
|
|
284
|
+
if (settingsReturnFocus && settingsReturnFocus.isConnected) settingsReturnFocus.focus();
|
|
285
|
+
settingsReturnFocus = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
openSettingsBtn.addEventListener("click", openSettings);
|
|
289
|
+
closeSettingsBtn.addEventListener("click", closeSettings);
|
|
290
|
+
cancelSettingsBtn.addEventListener("click", closeSettings);
|
|
291
|
+
settingsBackdrop.addEventListener("click", (event) => {
|
|
292
|
+
if (event.target === settingsBackdrop) closeSettings();
|
|
293
|
+
});
|
|
294
|
+
settingsForm.addEventListener("submit", (event) => {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
const readingLayout = readingLayoutInputs.find((input) => input.checked)?.value || "split";
|
|
297
|
+
const theme = themeInputs.find((input) => input.checked)?.value || "system";
|
|
298
|
+
applyReadingLayout(readingLayout);
|
|
299
|
+
applyThemePreference(theme);
|
|
300
|
+
closeSettings();
|
|
301
|
+
});
|
|
302
|
+
settingsBackdrop.addEventListener("keydown", (event) => {
|
|
303
|
+
if (event.key === "Escape") {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
closeSettings();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (event.key !== "Tab") return;
|
|
309
|
+
const focusable = [...settingsBackdrop.querySelectorAll('button:not([disabled]), input:not([disabled])')];
|
|
310
|
+
if (!focusable.length) return;
|
|
311
|
+
const first = focusable[0];
|
|
312
|
+
const last = focusable[focusable.length - 1];
|
|
313
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
last.focus();
|
|
316
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
first.focus();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
systemThemeMedia.addEventListener("change", () => {
|
|
322
|
+
if (state.themePreference === "system") applyThemePreference("system", false);
|
|
323
|
+
});
|
|
324
|
+
|
|
198
325
|
|
|
199
326
|
// ─── Data Fetching ──────────────────────────────────────────────────────────
|
|
200
327
|
|
|
@@ -259,7 +386,7 @@
|
|
|
259
386
|
async function fetchData() {
|
|
260
387
|
try {
|
|
261
388
|
const endpoint = state.viewMode === "threads" ? "/api/threads" : "/api/messages";
|
|
262
|
-
const currentPersona =
|
|
389
|
+
const currentPersona = getActiveSender();
|
|
263
390
|
const params = new URLSearchParams({
|
|
264
391
|
account: state.activeAccount,
|
|
265
392
|
persona: currentPersona,
|
|
@@ -292,32 +419,39 @@
|
|
|
292
419
|
// ─── Google Account Dropdown ────────────────────────────────────────────────
|
|
293
420
|
|
|
294
421
|
function renderAccountDropdown(agents) {
|
|
422
|
+
const allActive = state.activeAccount === "all";
|
|
295
423
|
let html = `
|
|
296
|
-
<button class="account-item-btn ${
|
|
297
|
-
<
|
|
298
|
-
<
|
|
299
|
-
<
|
|
300
|
-
<strong>God Mode
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
|
|
424
|
+
<button class="account-item-btn ${allActive ? "active" : ""}" data-handle="all" aria-current="${allActive ? "true" : "false"}">
|
|
425
|
+
<span class="account-item-left">
|
|
426
|
+
<span class="item-avatar" style="background:var(--primary-blue);color:#fff;">👑</span>
|
|
427
|
+
<span class="account-item-copy">
|
|
428
|
+
<strong class="account-item-name">God Mode</strong>
|
|
429
|
+
<span class="account-item-role">All accounts · sending as user</span>
|
|
430
|
+
<span class="account-item-address">amq://all-agents</span>
|
|
431
|
+
</span>
|
|
432
|
+
</span>
|
|
304
433
|
</button>
|
|
305
434
|
`;
|
|
306
435
|
|
|
307
|
-
for (const
|
|
308
|
-
const active = state.activeAccount ===
|
|
309
|
-
const
|
|
310
|
-
const
|
|
436
|
+
for (const agent of agents) {
|
|
437
|
+
const active = state.activeAccount === agent.handle;
|
|
438
|
+
const profile = agent.profile || {};
|
|
439
|
+
const name = profile.name || agent.handle;
|
|
440
|
+
const role = profile.role || "Swarm Agent";
|
|
441
|
+
const emoji = profile.emoji || agent.handle.slice(0, 1).toUpperCase();
|
|
442
|
+
const color = safeCssColor(profile.color, "#1a73e8");
|
|
443
|
+
const unreadBadge = agent.unreadCount > 0 ? `<span class="badge">${agent.unreadCount} new</span>` : "";
|
|
311
444
|
|
|
312
445
|
html += `
|
|
313
|
-
<button class="account-item-btn ${active ? "active" : ""}" data-handle="${
|
|
314
|
-
<
|
|
315
|
-
<
|
|
316
|
-
<
|
|
317
|
-
<strong>${
|
|
318
|
-
<
|
|
319
|
-
|
|
320
|
-
|
|
446
|
+
<button class="account-item-btn ${active ? "active" : ""}" data-handle="${escapeHtml(agent.handle)}" aria-current="${active ? "true" : "false"}" aria-label="Switch to ${escapeHtml(name)}">
|
|
447
|
+
<span class="account-item-left">
|
|
448
|
+
<span class="item-avatar" style="background:${escapeHtml(color)};color:#fff;">${escapeHtml(emoji)}</span>
|
|
449
|
+
<span class="account-item-copy">
|
|
450
|
+
<strong class="account-item-name">${escapeHtml(name)}</strong>
|
|
451
|
+
<span class="account-item-role">${escapeHtml(role)}</span>
|
|
452
|
+
<span class="account-item-address">${escapeHtml(agent.handle)}@amq</span>
|
|
453
|
+
</span>
|
|
454
|
+
</span>
|
|
321
455
|
${unreadBadge}
|
|
322
456
|
</button>
|
|
323
457
|
`;
|
|
@@ -329,20 +463,17 @@
|
|
|
329
463
|
btn.addEventListener("click", () => {
|
|
330
464
|
switchActiveAccount(btn.dataset.handle);
|
|
331
465
|
accountDropdown.classList.add("hidden");
|
|
466
|
+
userProfileBtn.setAttribute("aria-expanded", "false");
|
|
332
467
|
});
|
|
333
468
|
});
|
|
334
469
|
}
|
|
335
470
|
|
|
336
471
|
function switchActiveAccount(handle) {
|
|
337
472
|
state.activeAccount = handle;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
localStorage.setItem("agmail_persona", handle);
|
|
341
|
-
}
|
|
473
|
+
state.activePersona = handle === "all" ? "user" : handle;
|
|
474
|
+
localStorage.setItem("agmail_persona", state.activePersona);
|
|
342
475
|
|
|
343
|
-
const currentPersona =
|
|
344
|
-
const personaObj = state.agents.find((x) => x.handle === currentPersona);
|
|
345
|
-
const personaName = personaObj?.profile?.name || currentPersona;
|
|
476
|
+
const currentPersona = getActiveSender();
|
|
346
477
|
|
|
347
478
|
if (handle === "all") {
|
|
348
479
|
headerAccountLabel.textContent = "👑 God Mode";
|
|
@@ -350,7 +481,8 @@
|
|
|
350
481
|
currentUserAvatar.style.backgroundColor = "var(--primary-blue)";
|
|
351
482
|
dropdownLargeAvatar.textContent = "👑";
|
|
352
483
|
dropdownLargeAvatar.style.backgroundColor = "var(--primary-blue)";
|
|
353
|
-
dropdownUserTitle.textContent =
|
|
484
|
+
dropdownUserTitle.textContent = "God Mode";
|
|
485
|
+
dropdownUserRole.textContent = "All transmissions · sending as user";
|
|
354
486
|
dropdownUserEmail.textContent = "amq://all-agents";
|
|
355
487
|
} else {
|
|
356
488
|
const agentObj = state.agents.find((x) => x.handle === handle);
|
|
@@ -360,8 +492,9 @@
|
|
|
360
492
|
currentUserAvatar.style.backgroundColor = prof.color;
|
|
361
493
|
dropdownLargeAvatar.textContent = prof.emoji;
|
|
362
494
|
dropdownLargeAvatar.style.backgroundColor = prof.color;
|
|
363
|
-
dropdownUserTitle.textContent =
|
|
364
|
-
|
|
495
|
+
dropdownUserTitle.textContent = prof.name;
|
|
496
|
+
dropdownUserRole.textContent = prof.role || "Swarm Agent";
|
|
497
|
+
dropdownUserEmail.textContent = `${handle}@amq`;
|
|
365
498
|
}
|
|
366
499
|
|
|
367
500
|
replyAsUserEl.textContent = currentPersona;
|
|
@@ -372,62 +505,199 @@
|
|
|
372
505
|
// Toggle account dropdown on user profile click
|
|
373
506
|
userProfileBtn.addEventListener("click", (e) => {
|
|
374
507
|
e.stopPropagation();
|
|
508
|
+
const opening = accountDropdown.classList.contains("hidden");
|
|
375
509
|
accountDropdown.classList.toggle("hidden");
|
|
510
|
+
userProfileBtn.setAttribute("aria-expanded", String(opening));
|
|
376
511
|
});
|
|
377
512
|
|
|
378
513
|
// Close dropdown on outside click
|
|
379
514
|
document.addEventListener("click", (e) => {
|
|
380
515
|
if (!accountDropdown.contains(e.target) && !userProfileBtn.contains(e.target)) {
|
|
381
516
|
accountDropdown.classList.add("hidden");
|
|
517
|
+
userProfileBtn.setAttribute("aria-expanded", "false");
|
|
382
518
|
}
|
|
383
519
|
});
|
|
384
520
|
|
|
385
521
|
// ─── Swarm Presence List ────────────────────────────────────────────────────
|
|
386
522
|
|
|
523
|
+
function safeCssColor(value, fallback = "#1a73e8") {
|
|
524
|
+
const color = String(value || "").trim();
|
|
525
|
+
return /^#[0-9a-f]{3,8}$/i.test(color) ? color : fallback;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function agentStatusView(agent) {
|
|
529
|
+
const raw = String(agent?.herdrStatus && agent.herdrStatus !== "unknown" ? agent.herdrStatus : agent?.status || "offline").toLowerCase();
|
|
530
|
+
const status = raw === "online" || raw === "active" ? "idle" : raw;
|
|
531
|
+
const views = {
|
|
532
|
+
working: { label: "Working", tone: "working" },
|
|
533
|
+
idle: { label: "Idle", tone: "idle" },
|
|
534
|
+
done: { label: "Done", tone: "done" },
|
|
535
|
+
blocked: { label: "Blocked", tone: "blocked" },
|
|
536
|
+
error: { label: "Needs attention", tone: "blocked" },
|
|
537
|
+
offline: { label: "Offline", tone: "offline" },
|
|
538
|
+
};
|
|
539
|
+
return { status, ...(views[status] || views.offline) };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function getAgentTask(handle) {
|
|
543
|
+
if (!handle || !state.board?.columns) return null;
|
|
544
|
+
const rank = { in_progress: 0, blocked: 1, backlog: 2, done: 3 };
|
|
545
|
+
return Object.values(state.board.columns)
|
|
546
|
+
.flat()
|
|
547
|
+
.filter((task) => task.owner === handle)
|
|
548
|
+
.sort((left, right) => {
|
|
549
|
+
const rankDelta = (rank[left.status] ?? 9) - (rank[right.status] ?? 9);
|
|
550
|
+
if (rankDelta !== 0) return rankDelta;
|
|
551
|
+
return String(right.updated || right.created || "").localeCompare(String(left.updated || left.created || ""));
|
|
552
|
+
})[0] || null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function agentActivityText(agent, task) {
|
|
556
|
+
const activity = agent?.herdrActivity || {};
|
|
557
|
+
const statusView = agentStatusView(agent);
|
|
558
|
+
const stateLabel = activity.stateLabels?.[statusView.status] || activity.stateLabels?.[agent?.herdrStatus] || "";
|
|
559
|
+
const token = Array.isArray(activity.tokens) ? activity.tokens.find((value) => typeof value === "string" && value.trim()) : "";
|
|
560
|
+
const title = [activity.title, activity.terminalTitle, agent?.herdrTitle]
|
|
561
|
+
.find((value) => {
|
|
562
|
+
const text = String(value || "").trim();
|
|
563
|
+
return text && !/^(opencode|codex|claude code|agent|herdr)$/i.test(text);
|
|
564
|
+
}) || "";
|
|
565
|
+
if (stateLabel) return stateLabel;
|
|
566
|
+
if (token) return token;
|
|
567
|
+
if (title) return title;
|
|
568
|
+
if (task?.title) return task.title;
|
|
569
|
+
if (agent?.herdrStatus && agent.herdrStatus !== "unknown") return statusView.tone === "working" ? "Working in Herdr" : "Live in Herdr";
|
|
570
|
+
return "No live Herdr activity signal";
|
|
571
|
+
}
|
|
572
|
+
|
|
387
573
|
function renderPresenceList(agents) {
|
|
574
|
+
const priority = { working: 0, blocked: 1, idle: 2, done: 3, error: 4, offline: 5 };
|
|
575
|
+
const sorted = [...agents].sort((left, right) => {
|
|
576
|
+
const statusDelta = (priority[agentStatusView(left).status] ?? 9) - (priority[agentStatusView(right).status] ?? 9);
|
|
577
|
+
return statusDelta || left.handle.localeCompare(right.handle);
|
|
578
|
+
});
|
|
388
579
|
let html = "";
|
|
389
|
-
for (const
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (effectiveStatus === "working") {
|
|
400
|
-
statusClass = "working";
|
|
401
|
-
statusLabel = "working";
|
|
402
|
-
} else if (effectiveStatus === "idle" || effectiveStatus === "done" || effectiveStatus === "active") {
|
|
403
|
-
statusClass = "idle";
|
|
404
|
-
statusLabel = effectiveStatus;
|
|
405
|
-
} else if (effectiveStatus === "blocked" || effectiveStatus === "error") {
|
|
406
|
-
statusClass = "blocked";
|
|
407
|
-
statusLabel = effectiveStatus;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const isLive = Boolean(a.herdrStatus && a.herdrStatus !== "unknown");
|
|
411
|
-
const titleText = a.herdrTitle
|
|
412
|
-
? `${escapeHtml(prof.name)} (${escapeHtml(prof.role)}) • ${statusLabel} — ${escapeHtml(a.herdrTitle)}`
|
|
413
|
-
: `${escapeHtml(prof.name)} (${escapeHtml(prof.role)}) [${escapeHtml(prof.model || "model")}]: ${statusLabel}`;
|
|
580
|
+
for (const agent of sorted) {
|
|
581
|
+
const profile = agent.profile || {};
|
|
582
|
+
const name = profile.name || agent.handle;
|
|
583
|
+
const role = profile.role || "Swarm Agent";
|
|
584
|
+
const emoji = profile.emoji || agent.handle.slice(0, 1).toUpperCase();
|
|
585
|
+
const color = safeCssColor(profile.color);
|
|
586
|
+
const statusView = agentStatusView(agent);
|
|
587
|
+
const live = Boolean(agent.herdrStatus && agent.herdrStatus !== "unknown");
|
|
588
|
+
const task = getAgentTask(agent.handle);
|
|
589
|
+
const activity = agentActivityText(agent, task);
|
|
414
590
|
|
|
415
591
|
html += `
|
|
416
|
-
<
|
|
417
|
-
<
|
|
418
|
-
<
|
|
419
|
-
<span class="presence-dot ${
|
|
420
|
-
</
|
|
421
|
-
<
|
|
422
|
-
<span class="presence-name
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
592
|
+
<button type="button" class="presence-item${live ? " presence-live" : ""}" data-agent-handle="${escapeHtml(agent.handle)}" aria-haspopup="dialog" aria-label="View activity for ${escapeHtml(name)}, ${statusView.label}" title="${escapeHtml(activity)}">
|
|
593
|
+
<span class="presence-avatar-wrap">
|
|
594
|
+
<span class="mini-avatar" style="background:${escapeHtml(color)};color:#fff;">${escapeHtml(emoji)}</span>
|
|
595
|
+
<span class="presence-dot ${statusView.tone}"></span>
|
|
596
|
+
</span>
|
|
597
|
+
<span class="presence-details">
|
|
598
|
+
<span class="presence-name-row">
|
|
599
|
+
<span class="presence-name">${escapeHtml(agent.handle)}</span>
|
|
600
|
+
<span class="presence-status-pill ${statusView.tone}">${statusView.label}</span>
|
|
601
|
+
</span>
|
|
602
|
+
<span class="presence-role">${escapeHtml(role)}</span>
|
|
603
|
+
</span>
|
|
604
|
+
<span class="presence-chevron" aria-hidden="true">›</span>
|
|
605
|
+
</button>
|
|
426
606
|
`;
|
|
427
607
|
}
|
|
428
608
|
presenceListEl.innerHTML = html;
|
|
609
|
+
presenceListEl.querySelectorAll(".presence-item").forEach((button) => {
|
|
610
|
+
button.addEventListener("click", () => openAgentActivity(button.dataset.agentHandle));
|
|
611
|
+
});
|
|
612
|
+
if (agentActivityDialog?.open) renderAgentActivity();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function renderAgentActivity() {
|
|
616
|
+
const handle = state.activeAgentHandle;
|
|
617
|
+
const agent = state.agents.find((item) => item.handle === handle);
|
|
618
|
+
if (!agent || !agentActivityDialog) return false;
|
|
619
|
+
const profile = agent.profile || {};
|
|
620
|
+
const statusView = agentStatusView(agent);
|
|
621
|
+
const task = getAgentTask(handle);
|
|
622
|
+
const activity = agent.herdrActivity || {};
|
|
623
|
+
const title = String(activity.title || activity.terminalTitle || agent.herdrTitle || "").trim();
|
|
624
|
+
const observedAt = activity.observedAt || agent.herdrObservedAt;
|
|
625
|
+
const taskStatus = task ? task.status.replaceAll("_", " ") : "No active task assigned";
|
|
626
|
+
const taskTone = { in_progress: "working", blocked: "blocked", done: "done", backlog: "idle" }[task?.status] || "offline";
|
|
627
|
+
|
|
628
|
+
agentActivityAvatar.textContent = profile.emoji || handle.slice(0, 1).toUpperCase();
|
|
629
|
+
agentActivityAvatar.style.backgroundColor = safeCssColor(profile.color);
|
|
630
|
+
agentActivityName.textContent = profile.name || handle;
|
|
631
|
+
agentActivityHandle.textContent = `${handle}@amq`;
|
|
632
|
+
agentActivityRole.textContent = profile.role || "Swarm Agent";
|
|
633
|
+
agentActivityStatus.textContent = statusView.label;
|
|
634
|
+
agentActivityStatus.className = `agent-activity-status ${statusView.tone}`;
|
|
635
|
+
agentActivityHeadline.textContent = agentActivityText(agent, task);
|
|
636
|
+
agentActivityTask.textContent = task?.title || "No board task is currently assigned to this agent.";
|
|
637
|
+
agentActivityTaskStatus.textContent = taskStatus;
|
|
638
|
+
agentActivityTaskStatus.className = `agent-task-status ${taskTone}`;
|
|
639
|
+
const liveModel = agent.runtimeModel || activity.model || "";
|
|
640
|
+
const modelSource = agent.modelSource || activity.modelSource || "";
|
|
641
|
+
agentActivityModel.textContent = liveModel || profile.model || "Not configured";
|
|
642
|
+
agentActivityModel.title = liveModel
|
|
643
|
+
? `Live harness model${modelSource ? ` · ${modelSource}` : ""}`
|
|
644
|
+
: profile.model
|
|
645
|
+
? "Configured profile model"
|
|
646
|
+
: "No model reported by the harness or profile";
|
|
647
|
+
agentActivityUnread.textContent = String(agent.unreadCount || 0);
|
|
648
|
+
agentActivityPane.textContent = agent.herdrPaneId || "Not tracked";
|
|
649
|
+
agentActivityObserved.textContent = observedAt ? new Date(observedAt).toLocaleString() : "No live snapshot";
|
|
650
|
+
viewAgentTaskBtn.hidden = !task;
|
|
651
|
+
agentActivityDialog.dataset.agentHandle = handle;
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function openAgentActivity(handle) {
|
|
656
|
+
state.activeAgentHandle = handle;
|
|
657
|
+
if (!renderAgentActivity()) {
|
|
658
|
+
state.activeAgentHandle = null;
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (agentActivityDialog.open) return;
|
|
662
|
+
if (typeof agentActivityDialog.showModal === "function") agentActivityDialog.showModal();
|
|
663
|
+
else agentActivityDialog.setAttribute("open", "");
|
|
664
|
+
requestAnimationFrame(() => closeAgentActivityBtn?.focus());
|
|
429
665
|
}
|
|
430
666
|
|
|
667
|
+
function closeAgentActivity() {
|
|
668
|
+
if (typeof agentActivityDialog.close === "function") agentActivityDialog.close();
|
|
669
|
+
else agentActivityDialog.removeAttribute("open");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
closeAgentActivityBtn?.addEventListener("click", closeAgentActivity);
|
|
673
|
+
agentActivityDialog?.addEventListener("click", (event) => {
|
|
674
|
+
if (event.target !== agentActivityDialog) return;
|
|
675
|
+
const bounds = agentActivityDialog.getBoundingClientRect();
|
|
676
|
+
const outside = event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom;
|
|
677
|
+
if (outside) closeAgentActivity();
|
|
678
|
+
});
|
|
679
|
+
agentActivityDialog?.addEventListener("close", () => {
|
|
680
|
+
state.activeAgentHandle = null;
|
|
681
|
+
});
|
|
682
|
+
viewAgentInboxBtn?.addEventListener("click", () => {
|
|
683
|
+
const handle = state.activeAgentHandle;
|
|
684
|
+
closeAgentActivity();
|
|
685
|
+
if (!handle) return;
|
|
686
|
+
switchActiveAccount(handle);
|
|
687
|
+
switchView("mail");
|
|
688
|
+
if (window.innerWidth <= 768) {
|
|
689
|
+
document.body.classList.remove("sidebar-open");
|
|
690
|
+
sidebarBackdrop.classList.add("hidden");
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
viewAgentTaskBtn?.addEventListener("click", () => {
|
|
694
|
+
const task = getAgentTask(state.activeAgentHandle);
|
|
695
|
+
closeAgentActivity();
|
|
696
|
+
if (!task) return;
|
|
697
|
+
switchView("board");
|
|
698
|
+
requestAnimationFrame(() => openTaskSheet(task.id));
|
|
699
|
+
});
|
|
700
|
+
|
|
431
701
|
|
|
432
702
|
function levenshtein(a, b) {
|
|
433
703
|
if (a.length === 0) return b.length;
|
|
@@ -558,7 +828,7 @@
|
|
|
558
828
|
if (prevPageBtn) prevPageBtn.disabled = state.page <= 1;
|
|
559
829
|
if (nextPageBtn) nextPageBtn.disabled = state.page >= state.totalPages;
|
|
560
830
|
|
|
561
|
-
const currentPersona =
|
|
831
|
+
const currentPersona = getActiveSender();
|
|
562
832
|
filterInfoEl.textContent = `Account: ${state.activeAccount} • Persona: ${currentPersona} • Mode: ${state.viewMode}`;
|
|
563
833
|
|
|
564
834
|
if (!filtered.length) {
|
|
@@ -578,6 +848,7 @@
|
|
|
578
848
|
for (const t of threads) {
|
|
579
849
|
const isUnread = t.hasUnread;
|
|
580
850
|
const isStarred = state.starredIds.has(t.threadId);
|
|
851
|
+
const isSelected = state.selectedThreadId === t.threadId;
|
|
581
852
|
const dateStr = formatTimestamp(t.latestCreated);
|
|
582
853
|
|
|
583
854
|
const senders = t.participants.join(", ");
|
|
@@ -594,7 +865,7 @@
|
|
|
594
865
|
const sendersHighlighted = highlightTerms(escapeHtml(senders), state.searchQuery);
|
|
595
866
|
|
|
596
867
|
html += `
|
|
597
|
-
<div class="mail-row ${isUnread ? "unread" : "read"}" data-thread-id="${t.threadId}">
|
|
868
|
+
<div class="mail-row ${isUnread ? "unread" : "read"} ${isSelected ? "selected" : ""}" data-thread-id="${t.threadId}" role="button" tabindex="0" aria-current="${isSelected ? "true" : "false"}">
|
|
598
869
|
<div class="mail-row-actions">
|
|
599
870
|
<button class="star-btn ${isStarred ? "starred" : ""}" data-star-id="${t.threadId}" title="Star thread">
|
|
600
871
|
${isStarred ? "★" : "☆"}
|
|
@@ -616,11 +887,19 @@
|
|
|
616
887
|
mailListEl.innerHTML = html;
|
|
617
888
|
|
|
618
889
|
mailListEl.querySelectorAll(".mail-row").forEach((row) => {
|
|
619
|
-
|
|
620
|
-
if (e.target.closest(".star-btn")) return;
|
|
890
|
+
const activate = () => {
|
|
621
891
|
const threadId = row.dataset.threadId;
|
|
622
892
|
const thread = state.items.find((t) => t.threadId === threadId);
|
|
623
893
|
if (thread) openThread(thread);
|
|
894
|
+
};
|
|
895
|
+
row.addEventListener("click", (e) => {
|
|
896
|
+
if (!e.target.closest(".star-btn")) activate();
|
|
897
|
+
});
|
|
898
|
+
row.addEventListener("keydown", (e) => {
|
|
899
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
900
|
+
e.preventDefault();
|
|
901
|
+
activate();
|
|
902
|
+
}
|
|
624
903
|
});
|
|
625
904
|
});
|
|
626
905
|
|
|
@@ -632,6 +911,7 @@
|
|
|
632
911
|
for (const m of messages) {
|
|
633
912
|
const isStarred = state.starredIds.has(m.id);
|
|
634
913
|
const isUnread = m.isNew;
|
|
914
|
+
const isSelected = state.selectedMessageId === m.id;
|
|
635
915
|
const dateStr = formatTimestamp(m.created);
|
|
636
916
|
|
|
637
917
|
let badgesHtml = "";
|
|
@@ -645,7 +925,7 @@
|
|
|
645
925
|
const senderHighlighted = highlightTerms(escapeHtml(m.from || "unknown"), state.searchQuery);
|
|
646
926
|
|
|
647
927
|
html += `
|
|
648
|
-
<div class="mail-row ${isUnread ? "unread" : "read"}" data-msg-id="${m.id}">
|
|
928
|
+
<div class="mail-row ${isUnread ? "unread" : "read"} ${isSelected ? "selected" : ""}" data-msg-id="${m.id}" role="button" tabindex="0" aria-current="${isSelected ? "true" : "false"}">
|
|
649
929
|
<div class="mail-row-actions">
|
|
650
930
|
<button class="star-btn ${isStarred ? "starred" : ""}" data-star-id="${m.id}" title="Star message">
|
|
651
931
|
${isStarred ? "★" : "☆"}
|
|
@@ -667,11 +947,19 @@
|
|
|
667
947
|
mailListEl.innerHTML = html;
|
|
668
948
|
|
|
669
949
|
mailListEl.querySelectorAll(".mail-row").forEach((row) => {
|
|
670
|
-
|
|
671
|
-
if (e.target.closest(".star-btn")) return;
|
|
950
|
+
const activate = () => {
|
|
672
951
|
const msgId = row.dataset.msgId;
|
|
673
952
|
const msg = state.items.find((m) => m.id === msgId);
|
|
674
953
|
if (msg) openSingleMessage(msg);
|
|
954
|
+
};
|
|
955
|
+
row.addEventListener("click", (e) => {
|
|
956
|
+
if (!e.target.closest(".star-btn")) activate();
|
|
957
|
+
});
|
|
958
|
+
row.addEventListener("keydown", (e) => {
|
|
959
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
960
|
+
e.preventDefault();
|
|
961
|
+
activate();
|
|
962
|
+
}
|
|
675
963
|
});
|
|
676
964
|
});
|
|
677
965
|
|
|
@@ -694,6 +982,66 @@
|
|
|
694
982
|
});
|
|
695
983
|
}
|
|
696
984
|
|
|
985
|
+
function syncSelectedMailRow() {
|
|
986
|
+
mailListEl.querySelectorAll(".mail-row").forEach((row) => {
|
|
987
|
+
const selected = state.viewMode === "threads"
|
|
988
|
+
? row.dataset.threadId === state.selectedThreadId
|
|
989
|
+
: row.dataset.msgId === state.selectedMessageId;
|
|
990
|
+
row.classList.toggle("selected", selected);
|
|
991
|
+
row.setAttribute("aria-current", selected ? "true" : "false");
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function showMessageDetail() {
|
|
996
|
+
state.detailOpen = true;
|
|
997
|
+
contentSplitterEl.classList.add("detail-open");
|
|
998
|
+
mailDetailViewEl.classList.remove("hidden");
|
|
999
|
+
syncSelectedMailRow();
|
|
1000
|
+
mailDetailViewEl.scrollTop = 0;
|
|
1001
|
+
requestAnimationFrame(() => {
|
|
1002
|
+
const latestMessage = threadMessagesListEl.querySelector(".thread-card.expanded") || threadMessagesListEl.lastElementChild;
|
|
1003
|
+
if (latestMessage) {
|
|
1004
|
+
const detailTop = mailDetailViewEl.getBoundingClientRect().top;
|
|
1005
|
+
const headerHeight = mailDetailViewEl.querySelector(".detail-header")?.getBoundingClientRect().height || 0;
|
|
1006
|
+
const latestTop = latestMessage.getBoundingClientRect().top - detailTop;
|
|
1007
|
+
mailDetailViewEl.scrollTop = Math.max(0, latestTop - headerHeight - 12);
|
|
1008
|
+
if (state.readingLayout === "full" || window.matchMedia("(max-width: 768px)").matches) {
|
|
1009
|
+
latestMessage.tabIndex = -1;
|
|
1010
|
+
latestMessage.focus({ preventScroll: true });
|
|
1011
|
+
}
|
|
1012
|
+
} else if (state.readingLayout === "full" || window.matchMedia("(max-width: 768px)").matches) {
|
|
1013
|
+
mailDetailViewEl.focus({ preventScroll: true });
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function hideMessageDetail(restoreFocus = false) {
|
|
1019
|
+
state.detailOpen = false;
|
|
1020
|
+
contentSplitterEl.classList.remove("detail-open");
|
|
1021
|
+
mailDetailViewEl.classList.add("hidden");
|
|
1022
|
+
if (restoreFocus) {
|
|
1023
|
+
requestAnimationFrame(() => mailListEl.querySelector(".mail-row.selected")?.focus({ preventScroll: true }));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function markMessageRead(message) {
|
|
1028
|
+
if (!message?.isNew || state.activeAccount === "all") return;
|
|
1029
|
+
const account = message.targetAccount || state.activeAccount;
|
|
1030
|
+
if (!account || account === "all") return;
|
|
1031
|
+
try {
|
|
1032
|
+
const res = await fetch(`/api/messages/${encodeURIComponent(message.id)}/read?account=${encodeURIComponent(account)}`, { method: "POST" });
|
|
1033
|
+
if (!res.ok) return;
|
|
1034
|
+
message.isNew = false;
|
|
1035
|
+
const thread = state.items.find((item) => item.threadId === state.selectedThreadId);
|
|
1036
|
+
if (thread) thread.hasUnread = (thread.messages || []).some((item) => item.isNew);
|
|
1037
|
+
renderList();
|
|
1038
|
+
} catch {}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function markThreadRead(messages) {
|
|
1042
|
+
return Promise.all((messages || []).filter((message) => message.isNew).map((message) => markMessageRead(message)));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
697
1045
|
// ─── Open Thread (Conversation View) ────────────────────────────────────────
|
|
698
1046
|
|
|
699
1047
|
function openThread(thread) {
|
|
@@ -764,7 +1112,8 @@
|
|
|
764
1112
|
.map((r) => `<button class="chip" data-text="${escapeHtml(r)}">${escapeHtml(r)}</button>`)
|
|
765
1113
|
.join("");
|
|
766
1114
|
|
|
767
|
-
|
|
1115
|
+
void markThreadRead(msgs);
|
|
1116
|
+
showMessageDetail();
|
|
768
1117
|
}
|
|
769
1118
|
|
|
770
1119
|
function openSingleMessage(msg) {
|
|
@@ -814,7 +1163,8 @@
|
|
|
814
1163
|
.map((r) => `<button class="chip" data-text="${escapeHtml(r)}">${escapeHtml(r)}</button>`)
|
|
815
1164
|
.join("");
|
|
816
1165
|
|
|
817
|
-
|
|
1166
|
+
void markMessageRead(msg);
|
|
1167
|
+
showMessageDetail();
|
|
818
1168
|
}
|
|
819
1169
|
|
|
820
1170
|
// Expand / collapse all cards in thread
|
|
@@ -1126,7 +1476,7 @@
|
|
|
1126
1476
|
const existingTokens = new Set(parts.map((p) => p.toLowerCase()));
|
|
1127
1477
|
const activeToken = isTrailingSpace ? "" : (parts[parts.length - 1] || "").toLowerCase();
|
|
1128
1478
|
|
|
1129
|
-
const currentPersona =
|
|
1479
|
+
const currentPersona = getActiveSender();
|
|
1130
1480
|
const personaObj = Array.isArray(state.agents) ? state.agents.find((x) => x.handle === currentPersona) : null;
|
|
1131
1481
|
const personaTitle = personaObj?.profile?.name || currentPersona;
|
|
1132
1482
|
const personaAvatar = personaObj?.profile?.emoji || "👤";
|
|
@@ -1388,7 +1738,7 @@
|
|
|
1388
1738
|
});
|
|
1389
1739
|
|
|
1390
1740
|
backToListBtn.addEventListener("click", () => {
|
|
1391
|
-
|
|
1741
|
+
hideMessageDetail(true);
|
|
1392
1742
|
});
|
|
1393
1743
|
|
|
1394
1744
|
refreshBtn.addEventListener("click", () => {
|
|
@@ -1422,7 +1772,7 @@
|
|
|
1422
1772
|
sendQuickReplyBtn.textContent = "Sending...";
|
|
1423
1773
|
|
|
1424
1774
|
try {
|
|
1425
|
-
const currentPersona =
|
|
1775
|
+
const currentPersona = getActiveSender();
|
|
1426
1776
|
const res = await fetch("/api/reply", {
|
|
1427
1777
|
method: "POST",
|
|
1428
1778
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1470,8 +1820,9 @@
|
|
|
1470
1820
|
|
|
1471
1821
|
openComposeBtn.addEventListener("click", () => {
|
|
1472
1822
|
composeModalEl.classList.remove("hidden");
|
|
1473
|
-
const currentPersona =
|
|
1823
|
+
const currentPersona = getActiveSender();
|
|
1474
1824
|
composeFromEl.value = currentPersona;
|
|
1825
|
+
composeFromEl.disabled = state.activeAccount === "all";
|
|
1475
1826
|
composeSubjectEl.focus();
|
|
1476
1827
|
});
|
|
1477
1828
|
|
|
@@ -1502,7 +1853,7 @@
|
|
|
1502
1853
|
|
|
1503
1854
|
composeFormEl.addEventListener("submit", async (e) => {
|
|
1504
1855
|
e.preventDefault();
|
|
1505
|
-
const from = composeFromEl.value;
|
|
1856
|
+
const from = getActiveSender(composeFromEl.value);
|
|
1506
1857
|
const to = composeToEl.value;
|
|
1507
1858
|
const subject = composeSubjectEl.value;
|
|
1508
1859
|
const thread = composeThreadEl.value;
|
|
@@ -1567,21 +1918,17 @@
|
|
|
1567
1918
|
}
|
|
1568
1919
|
}
|
|
1569
1920
|
});
|
|
1921
|
+
} else if (payload.type === "herdr_agents_refresh") {
|
|
1922
|
+
fetchAgents();
|
|
1570
1923
|
} else if (payload.type === "herdr_agent_update") {
|
|
1571
|
-
// Fast path: update just this agent's status dot without full re-fetch
|
|
1572
1924
|
const { handle, herdrStatus } = payload;
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
agent.herdrStatus = herdrStatus;
|
|
1579
|
-
renderPresenceList(state.agents);
|
|
1580
|
-
} else {
|
|
1581
|
-
// Unknown agent — full refresh
|
|
1582
|
-
fetchAgents();
|
|
1583
|
-
}
|
|
1925
|
+
const agent = state.agents.find((item) => item.handle === handle);
|
|
1926
|
+
if (agent && herdrStatus) {
|
|
1927
|
+
agent.status = herdrStatus !== "unknown" ? herdrStatus : agent.status;
|
|
1928
|
+
agent.herdrStatus = herdrStatus;
|
|
1929
|
+
renderPresenceList(state.agents);
|
|
1584
1930
|
}
|
|
1931
|
+
fetchAgents();
|
|
1585
1932
|
}
|
|
1586
1933
|
} catch {}
|
|
1587
1934
|
};
|
|
@@ -2074,12 +2421,16 @@
|
|
|
2074
2421
|
const data = await res.json();
|
|
2075
2422
|
if (data.ok) {
|
|
2076
2423
|
state.board = data;
|
|
2424
|
+
if (agentActivityDialog?.open) renderAgentActivity();
|
|
2077
2425
|
if (boardTotalCountEl) {
|
|
2078
2426
|
boardTotalCountEl.textContent = data.stats?.total || 0;
|
|
2079
2427
|
}
|
|
2080
2428
|
if (state.currentView === "board") {
|
|
2081
2429
|
renderBoard();
|
|
2082
2430
|
}
|
|
2431
|
+
if (taskModalBackdrop && !taskModalBackdrop.classList.contains("hidden")) {
|
|
2432
|
+
renderTaskOwnerTip();
|
|
2433
|
+
}
|
|
2083
2434
|
}
|
|
2084
2435
|
} catch (err) {
|
|
2085
2436
|
console.warn("[board] Failed to fetch board:", err.message);
|
|
@@ -2350,7 +2701,7 @@
|
|
|
2350
2701
|
headers: { "Content-Type": "application/json" },
|
|
2351
2702
|
body: JSON.stringify({
|
|
2352
2703
|
status: targetCol,
|
|
2353
|
-
from:
|
|
2704
|
+
from: getActiveSender(),
|
|
2354
2705
|
notify: true,
|
|
2355
2706
|
}),
|
|
2356
2707
|
});
|
|
@@ -2375,19 +2726,42 @@
|
|
|
2375
2726
|
}
|
|
2376
2727
|
}
|
|
2377
2728
|
|
|
2729
|
+
function renderTaskOwnerTip() {
|
|
2730
|
+
if (!taskOwnerTip || !taskOwnerSelect) return;
|
|
2731
|
+
const owner = taskOwnerSelect.value;
|
|
2732
|
+
if (!owner) {
|
|
2733
|
+
taskOwnerTip.hidden = true;
|
|
2734
|
+
taskOwnerTip.textContent = "";
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const tasks = [...(state.board?.columns?.in_progress || []), ...(state.board?.columns?.blocked || [])]
|
|
2738
|
+
.filter((task) => task.owner === owner);
|
|
2739
|
+
const claimed = tasks.filter((task) => task.status === "in_progress");
|
|
2740
|
+
const blocked = tasks.filter((task) => task.status === "blocked");
|
|
2741
|
+
taskOwnerTip.hidden = false;
|
|
2742
|
+
taskOwnerTip.className = tasks.length ? "task-owner-tip warning" : "task-owner-tip";
|
|
2743
|
+
if (!tasks.length) {
|
|
2744
|
+
taskOwnerTip.textContent = `@${owner} has no claimed or blocked board tasks.`;
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const titles = tasks.slice(0, 2).map((task) => task.title).join(" · ");
|
|
2748
|
+
taskOwnerTip.textContent = `Before assigning: @${owner} has ${claimed.length} claimed and ${blocked.length} blocked. Review: ${titles}`;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2378
2751
|
function openNewTaskModal() {
|
|
2379
2752
|
if (!taskModalBackdrop) return;
|
|
2380
2753
|
|
|
2381
2754
|
// Populate assigned agent options
|
|
2382
2755
|
if (taskOwnerSelect) {
|
|
2383
2756
|
let optionsHtml = "";
|
|
2384
|
-
const currentPersona =
|
|
2757
|
+
const currentPersona = getActiveSender();
|
|
2385
2758
|
const handles = state.agents.map((a) => a.handle);
|
|
2386
2759
|
if (!handles.includes("coordinator")) handles.unshift("coordinator");
|
|
2760
|
+
if (!handles.includes("user")) handles.push("user");
|
|
2387
2761
|
|
|
2388
2762
|
for (const h of handles) {
|
|
2389
2763
|
const agentObj = state.agents.find((a) => a.handle === h);
|
|
2390
|
-
const name = agentObj?.profile?.name || h;
|
|
2764
|
+
const name = h === "user" ? "Human operator" : agentObj?.profile?.name || h;
|
|
2391
2765
|
const selected = h === currentPersona ? "selected" : "";
|
|
2392
2766
|
optionsHtml += `<option value="${escapeHtml(h)}" ${selected}>@${escapeHtml(h)} (${escapeHtml(name)})</option>`;
|
|
2393
2767
|
}
|
|
@@ -2401,6 +2775,7 @@
|
|
|
2401
2775
|
const notifyCheckbox = document.getElementById("task-notify-checkbox");
|
|
2402
2776
|
if (notifyCheckbox) notifyCheckbox.checked = true;
|
|
2403
2777
|
|
|
2778
|
+
renderTaskOwnerTip();
|
|
2404
2779
|
taskModalBackdrop.classList.remove("hidden");
|
|
2405
2780
|
taskTitleInput?.focus();
|
|
2406
2781
|
}
|
|
@@ -2415,13 +2790,14 @@
|
|
|
2415
2790
|
if (!title) return;
|
|
2416
2791
|
|
|
2417
2792
|
const notifyVal = document.getElementById("task-notify-checkbox")?.checked ?? true;
|
|
2793
|
+
const from = getActiveSender();
|
|
2418
2794
|
const payload = {
|
|
2419
2795
|
title,
|
|
2420
|
-
owner: taskOwnerSelect?.value ||
|
|
2796
|
+
owner: taskOwnerSelect?.value || from,
|
|
2421
2797
|
status: taskStatusSelect?.value || "backlog",
|
|
2422
2798
|
description: taskDescInput?.value.trim() || "",
|
|
2423
|
-
notify: notifyVal,
|
|
2424
|
-
from
|
|
2799
|
+
notify: notifyVal && taskOwnerSelect?.value !== from,
|
|
2800
|
+
from,
|
|
2425
2801
|
};
|
|
2426
2802
|
|
|
2427
2803
|
try {
|
|
@@ -2434,7 +2810,7 @@
|
|
|
2434
2810
|
if (data.ok) {
|
|
2435
2811
|
closeNewTaskModal();
|
|
2436
2812
|
await fetchBoard();
|
|
2437
|
-
const notifyMsg =
|
|
2813
|
+
const notifyMsg = payload.notify ? " & automail dispatched" : "";
|
|
2438
2814
|
showToast(`Task "${title.slice(0, 30)}" created${notifyMsg}!`);
|
|
2439
2815
|
} else {
|
|
2440
2816
|
alert("Failed to create task: " + (data.error || "unknown"));
|
|
@@ -2657,7 +3033,7 @@
|
|
|
2657
3033
|
|
|
2658
3034
|
if (sheetDispatchFrom) {
|
|
2659
3035
|
let fromOptions = "";
|
|
2660
|
-
const currentPersona =
|
|
3036
|
+
const currentPersona = getActiveSender();
|
|
2661
3037
|
for (const h of handles) {
|
|
2662
3038
|
const sel = h === currentPersona ? "selected" : "";
|
|
2663
3039
|
fromOptions += `<option value="${escapeHtml(h)}" ${sel}>${escapeHtml(h)}</option>`;
|
|
@@ -2746,6 +3122,7 @@
|
|
|
2746
3122
|
closeTaskModalBtn?.addEventListener("click", closeNewTaskModal);
|
|
2747
3123
|
cancelTaskBtn?.addEventListener("click", closeNewTaskModal);
|
|
2748
3124
|
newTaskForm?.addEventListener("submit", submitNewTask);
|
|
3125
|
+
taskOwnerSelect?.addEventListener("change", renderTaskOwnerTip);
|
|
2749
3126
|
|
|
2750
3127
|
taskModalBackdrop?.addEventListener("click", (e) => {
|
|
2751
3128
|
if (e.target === taskModalBackdrop) closeNewTaskModal();
|
|
@@ -2816,7 +3193,7 @@
|
|
|
2816
3193
|
headers: { "Content-Type": "application/json" },
|
|
2817
3194
|
body: JSON.stringify({
|
|
2818
3195
|
owner: newOwner,
|
|
2819
|
-
from:
|
|
3196
|
+
from: getActiveSender(),
|
|
2820
3197
|
notify: true,
|
|
2821
3198
|
}),
|
|
2822
3199
|
});
|
|
@@ -2857,7 +3234,7 @@
|
|
|
2857
3234
|
return;
|
|
2858
3235
|
}
|
|
2859
3236
|
|
|
2860
|
-
const fromVal = sheetDispatchFrom?.value ||
|
|
3237
|
+
const fromVal = getActiveSender(sheetDispatchFrom?.value || "");
|
|
2861
3238
|
const toVal = sheetDispatchTo?.value || "coordinator";
|
|
2862
3239
|
const task = findTaskById(state.selectedTaskId);
|
|
2863
3240
|
|
|
@@ -2952,13 +3329,16 @@
|
|
|
2952
3329
|
if (state.currentView === "board") {
|
|
2953
3330
|
return document.querySelector(".kanban-grid") || boardViewSection;
|
|
2954
3331
|
}
|
|
2955
|
-
|
|
3332
|
+
if (state.detailOpen && !mailDetailViewEl.classList.contains("hidden")) {
|
|
3333
|
+
return mailDetailViewEl;
|
|
3334
|
+
}
|
|
3335
|
+
return mailListViewEl;
|
|
2956
3336
|
}
|
|
2957
3337
|
|
|
2958
3338
|
function isAtTop() {
|
|
2959
3339
|
const container = getActiveScrollableContainer();
|
|
2960
3340
|
if (!container) return window.scrollY <= 0;
|
|
2961
|
-
return container.scrollTop <=
|
|
3341
|
+
return container.scrollTop <= 1 && window.scrollY <= 0;
|
|
2962
3342
|
}
|
|
2963
3343
|
|
|
2964
3344
|
async function triggerReload() {
|
|
@@ -3048,6 +3428,8 @@
|
|
|
3048
3428
|
}
|
|
3049
3429
|
|
|
3050
3430
|
// Boot
|
|
3431
|
+
applyThemePreference(state.themePreference, false);
|
|
3432
|
+
applyReadingLayout(state.readingLayout, false);
|
|
3051
3433
|
fetchStatus();
|
|
3052
3434
|
fetchAgents();
|
|
3053
3435
|
fetchBriefs();
|