@katyella/legio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
// Legio Web UI — ChatView component
|
|
2
|
+
// Preact+HTM component providing the full chat interface:
|
|
3
|
+
// - Task-based sidebar grouping conversations by beads issue
|
|
4
|
+
// - Conversations section for direct agent/coordinator/gateway chat
|
|
5
|
+
// - Message feed with thread grouping + smart scroll
|
|
6
|
+
// - Chat input with POST /api/mail/send integration
|
|
7
|
+
// No npm dependencies — uses CDN imports. Served as a static ES module.
|
|
8
|
+
|
|
9
|
+
import { ActivityCard, MessageBubble } from "../components/message-bubble.js";
|
|
10
|
+
import { fetchJson, postJson } from "../lib/api.js";
|
|
11
|
+
import {
|
|
12
|
+
html,
|
|
13
|
+
useCallback,
|
|
14
|
+
useEffect,
|
|
15
|
+
useLayoutEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from "../lib/preact-setup.js";
|
|
20
|
+
import { agentColor, inferCapability, isActivityMessage, timeAgo } from "../lib/utils.js";
|
|
21
|
+
|
|
22
|
+
// Issue status icon colors
|
|
23
|
+
const STATUS_ICON_COLORS = {
|
|
24
|
+
open: "text-blue-400",
|
|
25
|
+
in_progress: "text-yellow-400",
|
|
26
|
+
closed: "text-green-400",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Issue status icons (Unicode)
|
|
30
|
+
const STATUS_ICONS = {
|
|
31
|
+
open: "\u25CB",
|
|
32
|
+
in_progress: "\u25D0",
|
|
33
|
+
closed: "\u2713",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Status sort order for task groups
|
|
37
|
+
const STATUS_ORDER = { in_progress: 0, open: 1, closed: 2 };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format a timestamp as a time divider label.
|
|
41
|
+
* Shows "Today at HH:MM", "Yesterday at HH:MM", or "MMM DD at HH:MM".
|
|
42
|
+
*/
|
|
43
|
+
function formatTimeDivider(isoString) {
|
|
44
|
+
if (!isoString) return "";
|
|
45
|
+
const d = new Date(isoString);
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
48
|
+
const yesterdayStart = new Date(todayStart.getTime() - 86400000);
|
|
49
|
+
const hhmm = d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
50
|
+
if (d >= todayStart) return `Today at ${hhmm}`;
|
|
51
|
+
if (d >= yesterdayStart) return `Yesterday at ${hhmm}`;
|
|
52
|
+
const month = d.toLocaleString("default", { month: "short" });
|
|
53
|
+
return `${month} ${d.getDate()} at ${hhmm}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build task groups from agents, issues, and mail.
|
|
58
|
+
*
|
|
59
|
+
* @returns {{ taskGroups: Map, generalAgentNames: Set, agentTaskMap: Map }}
|
|
60
|
+
* - taskGroups: Map<beadId, { issue, agents[], agentNames: Set, msgCount, unreadCount }>
|
|
61
|
+
* - generalAgentNames: Set<string> of agents with no beadId
|
|
62
|
+
* - agentTaskMap: Map<agentName, beadId>
|
|
63
|
+
*/
|
|
64
|
+
function buildTaskGroups(agents, issues, mail) {
|
|
65
|
+
const taskGroups = new Map();
|
|
66
|
+
const generalAgentNames = new Set();
|
|
67
|
+
const agentTaskMap = new Map();
|
|
68
|
+
const issueMap = new Map(issues.map((i) => [i.id, i]));
|
|
69
|
+
|
|
70
|
+
for (const agent of agents) {
|
|
71
|
+
const beadId = agent.beadId;
|
|
72
|
+
if (!beadId) {
|
|
73
|
+
generalAgentNames.add(agent.agentName);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
agentTaskMap.set(agent.agentName, beadId);
|
|
77
|
+
|
|
78
|
+
if (!taskGroups.has(beadId)) {
|
|
79
|
+
taskGroups.set(beadId, {
|
|
80
|
+
issue: issueMap.get(beadId) || null,
|
|
81
|
+
agents: [],
|
|
82
|
+
agentNames: new Set(),
|
|
83
|
+
msgCount: 0,
|
|
84
|
+
unreadCount: 0,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const group = taskGroups.get(beadId);
|
|
88
|
+
group.agents.push(agent);
|
|
89
|
+
group.agentNames.add(agent.agentName);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Count messages per task
|
|
93
|
+
for (const msg of mail) {
|
|
94
|
+
const fromTask = agentTaskMap.get(msg.from);
|
|
95
|
+
const toTask = agentTaskMap.get(msg.to);
|
|
96
|
+
// Attribute message to the task of either participant (prefer from)
|
|
97
|
+
const taskId = fromTask || toTask;
|
|
98
|
+
if (taskId && taskGroups.has(taskId)) {
|
|
99
|
+
const group = taskGroups.get(taskId);
|
|
100
|
+
group.msgCount++;
|
|
101
|
+
if (!msg.read) group.unreadCount++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { taskGroups, generalAgentNames, agentTaskMap };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sort task groups: in_progress first, then open, then closed.
|
|
110
|
+
* Within same status, sort by unread count (desc) then msgCount (desc).
|
|
111
|
+
*/
|
|
112
|
+
function sortTaskGroups(taskGroups) {
|
|
113
|
+
return [...taskGroups.entries()].sort(([, a], [, b]) => {
|
|
114
|
+
const aStatus = a.issue?.status || "open";
|
|
115
|
+
const bStatus = b.issue?.status || "open";
|
|
116
|
+
const aOrder = STATUS_ORDER[aStatus] ?? 3;
|
|
117
|
+
const bOrder = STATUS_ORDER[bStatus] ?? 3;
|
|
118
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
119
|
+
if (b.unreadCount !== a.unreadCount) return b.unreadCount - a.unreadCount;
|
|
120
|
+
return b.msgCount - a.msgCount;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* ChatView — 3-panel chat interface with task-based sidebar.
|
|
126
|
+
*
|
|
127
|
+
* Accepts state via props or falls back to window.state (for integration
|
|
128
|
+
* with the existing app.js render cycle).
|
|
129
|
+
*
|
|
130
|
+
* @param {object} props
|
|
131
|
+
* @param {object} [props.state] - App state object (mail, agents, issues, etc.)
|
|
132
|
+
* @param {Function} [props.onSendMessage] - Send callback(from, to, subject, body, type)
|
|
133
|
+
*/
|
|
134
|
+
export function ChatView({ state: propState, onSendMessage: propOnSendMessage }) {
|
|
135
|
+
const appState = propState || (typeof window !== "undefined" ? window.state : null) || {};
|
|
136
|
+
const mail = appState.mail || [];
|
|
137
|
+
const agents = appState.agents || [];
|
|
138
|
+
const issues = appState.issues || [];
|
|
139
|
+
|
|
140
|
+
// UI state — local to this component, persisted across re-renders
|
|
141
|
+
const [selectedTask, setSelectedTask] = useState(null);
|
|
142
|
+
const [selectedAgent, setSelectedAgent] = useState(null);
|
|
143
|
+
const [expandedTasks, setExpandedTasks] = useState(() => new Set());
|
|
144
|
+
const [collapsedThreads, setCollapsedThreads] = useState(() => new Set());
|
|
145
|
+
|
|
146
|
+
// Conversation state — direct chat with coordinator/gateway/agents
|
|
147
|
+
// null | { type: 'coordinator' | 'gateway' | 'agent', name: string, label: string, capability?: string, state?: string }
|
|
148
|
+
const [selectedConversation, setSelectedConversation] = useState(null);
|
|
149
|
+
const [conversationHistory, setConversationHistory] = useState([]);
|
|
150
|
+
const [pendingConvMessages, setPendingConvMessages] = useState([]);
|
|
151
|
+
|
|
152
|
+
// Chat/All mode toggle
|
|
153
|
+
const [chatMode, setChatMode] = useState("chat");
|
|
154
|
+
|
|
155
|
+
// Chat input form state (simplified: no From, Subject, or Type)
|
|
156
|
+
const [toVal, setToVal] = useState("");
|
|
157
|
+
const [bodyVal, setBodyVal] = useState("");
|
|
158
|
+
const [sending, setSending] = useState(false);
|
|
159
|
+
const [sendError, setSendError] = useState("");
|
|
160
|
+
|
|
161
|
+
// Feed container ref for smart scroll
|
|
162
|
+
const feedRef = useRef(null);
|
|
163
|
+
// Track whether user is near the bottom of the feed
|
|
164
|
+
const isNearBottomRef = useRef(true);
|
|
165
|
+
|
|
166
|
+
// Keep "To" field pre-filled with selected agent (only when no conversation selected)
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (selectedConversation) return;
|
|
169
|
+
setToVal(selectedAgent || "");
|
|
170
|
+
}, [selectedAgent, selectedConversation]);
|
|
171
|
+
|
|
172
|
+
// Fetch conversation history when selectedConversation changes, then poll every 3s
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!selectedConversation) {
|
|
175
|
+
setConversationHistory([]);
|
|
176
|
+
setPendingConvMessages([]);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setPendingConvMessages([]);
|
|
181
|
+
setConversationHistory([]);
|
|
182
|
+
|
|
183
|
+
let url = null;
|
|
184
|
+
if (selectedConversation.type === "coordinator") {
|
|
185
|
+
url = "/api/coordinator/chat/history?limit=100";
|
|
186
|
+
} else if (selectedConversation.type === "agent") {
|
|
187
|
+
url = `/api/agents/${selectedConversation.name}/chat/history`;
|
|
188
|
+
}
|
|
189
|
+
// gateway has no history endpoint
|
|
190
|
+
|
|
191
|
+
if (!url) return;
|
|
192
|
+
|
|
193
|
+
const fetchHistory = () => {
|
|
194
|
+
fetchJson(url)
|
|
195
|
+
.then((data) => {
|
|
196
|
+
setConversationHistory(Array.isArray(data) ? data : data.messages || []);
|
|
197
|
+
})
|
|
198
|
+
.catch(() => {
|
|
199
|
+
// Gracefully handle 404s since backend may not be deployed yet
|
|
200
|
+
setConversationHistory([]);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
fetchHistory();
|
|
205
|
+
const intervalId = setInterval(fetchHistory, 3000);
|
|
206
|
+
return () => clearInterval(intervalId);
|
|
207
|
+
}, [selectedConversation]);
|
|
208
|
+
|
|
209
|
+
// Smart scroll: after every render, scroll to bottom only if near bottom
|
|
210
|
+
useLayoutEffect(() => {
|
|
211
|
+
const feed = feedRef.current;
|
|
212
|
+
if (feed && isNearBottomRef.current) {
|
|
213
|
+
feed.scrollTop = feed.scrollHeight;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const handleFeedScroll = useCallback(() => {
|
|
218
|
+
const feed = feedRef.current;
|
|
219
|
+
if (!feed) return;
|
|
220
|
+
isNearBottomRef.current = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 50;
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
// Build task groups (memoized)
|
|
224
|
+
const { taskGroups, generalAgentNames, agentTaskMap } = useMemo(
|
|
225
|
+
() => buildTaskGroups(agents, issues, mail),
|
|
226
|
+
[agents, issues, mail],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Sorted task group entries (memoized)
|
|
230
|
+
const sortedGroups = useMemo(() => sortTaskGroups(taskGroups), [taskGroups]);
|
|
231
|
+
|
|
232
|
+
// General (unassigned) message counts
|
|
233
|
+
const generalCounts = useMemo(() => {
|
|
234
|
+
let msgCount = 0;
|
|
235
|
+
let unreadCount = 0;
|
|
236
|
+
for (const msg of mail) {
|
|
237
|
+
if (!agentTaskMap.has(msg.from) && !agentTaskMap.has(msg.to)) {
|
|
238
|
+
msgCount++;
|
|
239
|
+
if (!msg.read) unreadCount++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { msgCount, unreadCount };
|
|
243
|
+
}, [mail, agentTaskMap]);
|
|
244
|
+
|
|
245
|
+
// Conversation entries for sidebar (memoized)
|
|
246
|
+
// Always shows Coordinator and Gateway; adds all active agents
|
|
247
|
+
const conversationEntries = useMemo(() => {
|
|
248
|
+
const entries = [
|
|
249
|
+
{ type: "coordinator", name: "coordinator", label: "Coordinator" },
|
|
250
|
+
{ type: "gateway", name: "gateway", label: "Gateway" },
|
|
251
|
+
];
|
|
252
|
+
for (const agent of agents) {
|
|
253
|
+
if (agent.agentName === "coordinator" || agent.agentName === "gateway") continue;
|
|
254
|
+
entries.push({
|
|
255
|
+
type: "agent",
|
|
256
|
+
name: agent.agentName,
|
|
257
|
+
label: agent.agentName,
|
|
258
|
+
capability: agent.capability,
|
|
259
|
+
state: agent.state,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return entries;
|
|
263
|
+
}, [agents]);
|
|
264
|
+
|
|
265
|
+
// ----- Message filtering and thread grouping -----
|
|
266
|
+
|
|
267
|
+
let filteredMessages;
|
|
268
|
+
|
|
269
|
+
if (selectedConversation) {
|
|
270
|
+
// Merge conversation history + relevant mail + pending optimistic messages
|
|
271
|
+
const target = selectedConversation.name;
|
|
272
|
+
const convMail = mail.filter((m) => {
|
|
273
|
+
const isRelevant = m.from === target || m.to === target;
|
|
274
|
+
const isHuman = !m.audience || m.audience === "human" || m.audience === "both";
|
|
275
|
+
return isRelevant && isHuman;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Deduplicate by id, sort chronologically
|
|
279
|
+
const seenIds = new Set();
|
|
280
|
+
const merged = [];
|
|
281
|
+
for (const m of [...conversationHistory, ...convMail, ...pendingConvMessages]) {
|
|
282
|
+
if (m.id && seenIds.has(m.id)) continue;
|
|
283
|
+
if (m.id) seenIds.add(m.id);
|
|
284
|
+
merged.push(m);
|
|
285
|
+
}
|
|
286
|
+
filteredMessages = merged;
|
|
287
|
+
} else {
|
|
288
|
+
filteredMessages = [...mail];
|
|
289
|
+
|
|
290
|
+
if (selectedAgent) {
|
|
291
|
+
// Filter to just this agent's messages
|
|
292
|
+
filteredMessages = filteredMessages.filter(
|
|
293
|
+
(m) => m.from === selectedAgent || m.to === selectedAgent,
|
|
294
|
+
);
|
|
295
|
+
} else if (selectedTask === "__general__") {
|
|
296
|
+
// Show messages where neither from nor to has a beadId
|
|
297
|
+
filteredMessages = filteredMessages.filter(
|
|
298
|
+
(m) => !agentTaskMap.has(m.from) && !agentTaskMap.has(m.to),
|
|
299
|
+
);
|
|
300
|
+
} else if (selectedTask) {
|
|
301
|
+
// Show messages for this task's agents
|
|
302
|
+
const group = taskGroups.get(selectedTask);
|
|
303
|
+
if (group) {
|
|
304
|
+
const agentNames = group.agentNames;
|
|
305
|
+
filteredMessages = filteredMessages.filter(
|
|
306
|
+
(m) => agentNames.has(m.from) || agentNames.has(m.to),
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
filteredMessages = [];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// In chat mode, hide protocol/activity messages (only for non-conversation views)
|
|
314
|
+
if (chatMode === "chat") {
|
|
315
|
+
filteredMessages = filteredMessages.filter((m) => {
|
|
316
|
+
// Use audience field when available (new API), fall back to type heuristic (backward compat)
|
|
317
|
+
if (m.audience) {
|
|
318
|
+
return m.audience === "human" || m.audience === "both";
|
|
319
|
+
}
|
|
320
|
+
return !isActivityMessage(m);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
filteredMessages.sort(
|
|
326
|
+
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// Root messages: no threadId, or threadId equals their own id
|
|
330
|
+
const roots = filteredMessages.filter((m) => !m.threadId || m.threadId === m.id);
|
|
331
|
+
|
|
332
|
+
// Reply map: threadId -> MailMessage[]
|
|
333
|
+
const replyMap = {};
|
|
334
|
+
filteredMessages.forEach((m) => {
|
|
335
|
+
if (m.threadId && m.threadId !== m.id) {
|
|
336
|
+
if (!replyMap[m.threadId]) replyMap[m.threadId] = [];
|
|
337
|
+
replyMap[m.threadId].push(m);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ----- Event handlers -----
|
|
342
|
+
|
|
343
|
+
const handleTaskClick = useCallback((taskId) => {
|
|
344
|
+
setSelectedTask(taskId);
|
|
345
|
+
setSelectedAgent(null);
|
|
346
|
+
setSelectedConversation(null);
|
|
347
|
+
// Auto-expand task on click (not for general section)
|
|
348
|
+
if (taskId && taskId !== "__general__") {
|
|
349
|
+
setExpandedTasks((prev) => {
|
|
350
|
+
const next = new Set(prev);
|
|
351
|
+
next.add(taskId);
|
|
352
|
+
return next;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}, []);
|
|
356
|
+
|
|
357
|
+
const handleTaskToggle = useCallback((e, taskId) => {
|
|
358
|
+
e.stopPropagation();
|
|
359
|
+
setExpandedTasks((prev) => {
|
|
360
|
+
const next = new Set(prev);
|
|
361
|
+
if (next.has(taskId)) {
|
|
362
|
+
next.delete(taskId);
|
|
363
|
+
} else {
|
|
364
|
+
next.add(taskId);
|
|
365
|
+
}
|
|
366
|
+
return next;
|
|
367
|
+
});
|
|
368
|
+
}, []);
|
|
369
|
+
|
|
370
|
+
const handleAgentClick = useCallback((e, agentName) => {
|
|
371
|
+
e.stopPropagation();
|
|
372
|
+
setSelectedAgent(agentName);
|
|
373
|
+
setSelectedConversation(null);
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
const handleAllMessagesClick = useCallback(() => {
|
|
377
|
+
setSelectedTask(null);
|
|
378
|
+
setSelectedAgent(null);
|
|
379
|
+
setSelectedConversation(null);
|
|
380
|
+
}, []);
|
|
381
|
+
|
|
382
|
+
const handleThreadToggle = useCallback((threadId) => {
|
|
383
|
+
setCollapsedThreads((prev) => {
|
|
384
|
+
const next = new Set(prev);
|
|
385
|
+
if (next.has(threadId)) {
|
|
386
|
+
next.delete(threadId);
|
|
387
|
+
} else {
|
|
388
|
+
next.add(threadId);
|
|
389
|
+
}
|
|
390
|
+
return next;
|
|
391
|
+
});
|
|
392
|
+
}, []);
|
|
393
|
+
|
|
394
|
+
const handleConversationClick = useCallback((conv) => {
|
|
395
|
+
setSelectedConversation(conv);
|
|
396
|
+
setSelectedTask(null);
|
|
397
|
+
setSelectedAgent(null);
|
|
398
|
+
}, []);
|
|
399
|
+
|
|
400
|
+
const handleSend = useCallback(async () => {
|
|
401
|
+
// Conversation mode: route to agent/coordinator/gateway endpoint
|
|
402
|
+
if (selectedConversation) {
|
|
403
|
+
if (!bodyVal.trim()) {
|
|
404
|
+
setSendError("Message body is required.");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
setSendError("");
|
|
408
|
+
setSending(true);
|
|
409
|
+
|
|
410
|
+
// Create optimistic pending message
|
|
411
|
+
const optimisticId = `pending-${Date.now()}`;
|
|
412
|
+
const optimistic = {
|
|
413
|
+
id: optimisticId,
|
|
414
|
+
from: "you",
|
|
415
|
+
to: selectedConversation.name,
|
|
416
|
+
subject: "",
|
|
417
|
+
body: bodyVal.trim(),
|
|
418
|
+
createdAt: new Date().toISOString(),
|
|
419
|
+
audience: "human",
|
|
420
|
+
_chatTarget: selectedConversation.name,
|
|
421
|
+
};
|
|
422
|
+
setPendingConvMessages((prev) => [...prev, optimistic]);
|
|
423
|
+
|
|
424
|
+
let url;
|
|
425
|
+
if (selectedConversation.type === "coordinator") {
|
|
426
|
+
url = "/api/coordinator/chat";
|
|
427
|
+
} else if (selectedConversation.type === "gateway") {
|
|
428
|
+
url = "/api/gateway/chat";
|
|
429
|
+
} else {
|
|
430
|
+
url = `/api/agents/${selectedConversation.name}/chat`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
await postJson(url, { text: bodyVal.trim() });
|
|
435
|
+
setBodyVal("");
|
|
436
|
+
// Keep optimistic message until the next poll picks up the real response
|
|
437
|
+
} catch (err) {
|
|
438
|
+
setSendError(err.message || "Send failed");
|
|
439
|
+
// Remove the failed optimistic message
|
|
440
|
+
setPendingConvMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
441
|
+
} finally {
|
|
442
|
+
setSending(false);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Standard mail mode
|
|
448
|
+
if (!toVal.trim() || !bodyVal.trim()) {
|
|
449
|
+
setSendError("To and body are required.");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
setSendError("");
|
|
453
|
+
setSending(true);
|
|
454
|
+
try {
|
|
455
|
+
const sendFn =
|
|
456
|
+
propOnSendMessage || (typeof window !== "undefined" ? window.sendChatMessage : null);
|
|
457
|
+
if (sendFn) {
|
|
458
|
+
await sendFn("orchestrator", toVal.trim(), "", bodyVal.trim(), "status");
|
|
459
|
+
} else {
|
|
460
|
+
// Direct fetch fallback when app.js globals are unavailable
|
|
461
|
+
const res = await fetch("/api/mail/send", {
|
|
462
|
+
method: "POST",
|
|
463
|
+
headers: { "Content-Type": "application/json" },
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
from: "orchestrator",
|
|
466
|
+
to: toVal.trim(),
|
|
467
|
+
subject: "",
|
|
468
|
+
body: bodyVal.trim(),
|
|
469
|
+
type: "status",
|
|
470
|
+
priority: "normal",
|
|
471
|
+
audience: "human",
|
|
472
|
+
}),
|
|
473
|
+
});
|
|
474
|
+
if (!res.ok) {
|
|
475
|
+
const err = await res.json().catch(() => ({}));
|
|
476
|
+
throw new Error(err.error || "Send failed");
|
|
477
|
+
}
|
|
478
|
+
const msg = await res.json();
|
|
479
|
+
if (typeof window !== "undefined" && window.state) {
|
|
480
|
+
window.state.mail.push(msg);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Clear body on success; preserve to
|
|
484
|
+
setBodyVal("");
|
|
485
|
+
} catch (err) {
|
|
486
|
+
setSendError(err.message || "Send failed");
|
|
487
|
+
} finally {
|
|
488
|
+
setSending(false);
|
|
489
|
+
}
|
|
490
|
+
}, [selectedConversation, toVal, bodyVal, propOnSendMessage]);
|
|
491
|
+
|
|
492
|
+
// ----- Header data -----
|
|
493
|
+
|
|
494
|
+
const selectedAgentData = selectedAgent
|
|
495
|
+
? agents.find((a) => a.agentName === selectedAgent)
|
|
496
|
+
: null;
|
|
497
|
+
|
|
498
|
+
const selectedTaskData =
|
|
499
|
+
selectedTask && selectedTask !== "__general__" ? taskGroups.get(selectedTask) : null;
|
|
500
|
+
|
|
501
|
+
// Unread count for messages in the current view
|
|
502
|
+
const unreadInView = filteredMessages.filter((m) => !m.read).length;
|
|
503
|
+
|
|
504
|
+
// Input field shared classes
|
|
505
|
+
const inputClass =
|
|
506
|
+
"bg-[#1a1a1a] border border-[#2a2a2a] rounded px-2 py-1 text-sm text-[#e5e5e5] placeholder-[#666] outline-none focus:border-[#E64415]";
|
|
507
|
+
|
|
508
|
+
const showGeneral = generalAgentNames.size > 0 || generalCounts.msgCount > 0;
|
|
509
|
+
|
|
510
|
+
// ----- Message feed rendering helpers -----
|
|
511
|
+
|
|
512
|
+
// Render a single message as ActivityCard or MessageBubble based on type.
|
|
513
|
+
function renderMessage(msg, showName, compact) {
|
|
514
|
+
// Use audience field when available, fall back to type-based detection
|
|
515
|
+
const isActivity = msg.audience ? msg.audience === "agent" : isActivityMessage(msg);
|
|
516
|
+
if (isActivity) {
|
|
517
|
+
return html`<${ActivityCard}
|
|
518
|
+
key=${msg.id}
|
|
519
|
+
event=${msg}
|
|
520
|
+
capability=${inferCapability(msg.from, agents) || inferCapability(msg.to, agents)}
|
|
521
|
+
/>`;
|
|
522
|
+
}
|
|
523
|
+
const capability = inferCapability(msg.from, agents);
|
|
524
|
+
const isUser = msg.from === "orchestrator" || msg.from === "you" || msg.from === "human";
|
|
525
|
+
return html`<${MessageBubble}
|
|
526
|
+
key=${msg.id}
|
|
527
|
+
msg=${msg}
|
|
528
|
+
capability=${capability || (isUser ? "coordinator" : null)}
|
|
529
|
+
isUser=${isUser}
|
|
530
|
+
showName=${showName}
|
|
531
|
+
compact=${compact}
|
|
532
|
+
/>`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Render root messages with grouping and time dividers.
|
|
536
|
+
// Tracks prevRootMsg via closure to determine showHeader and time gaps.
|
|
537
|
+
// Returns flat array so each root can emit [divider?, bubble].
|
|
538
|
+
let prevRootMsg = null;
|
|
539
|
+
const feedItems = roots.flatMap((root) => {
|
|
540
|
+
const replies = replyMap[root.id] || [];
|
|
541
|
+
|
|
542
|
+
// Show header when sender changes or gap > 2 minutes
|
|
543
|
+
const showHeader =
|
|
544
|
+
!prevRootMsg ||
|
|
545
|
+
prevRootMsg.from !== root.from ||
|
|
546
|
+
new Date(root.createdAt).getTime() - new Date(prevRootMsg.createdAt).getTime() > 120000;
|
|
547
|
+
|
|
548
|
+
// Time divider when gap > 30 minutes between root messages
|
|
549
|
+
const showDivider =
|
|
550
|
+
prevRootMsg &&
|
|
551
|
+
new Date(root.createdAt).getTime() - new Date(prevRootMsg.createdAt).getTime() > 1800000;
|
|
552
|
+
|
|
553
|
+
prevRootMsg = root;
|
|
554
|
+
|
|
555
|
+
const items = [];
|
|
556
|
+
|
|
557
|
+
if (showDivider) {
|
|
558
|
+
items.push(html`
|
|
559
|
+
<div key=${`divider-${root.id}`} class="flex items-center gap-3 my-4">
|
|
560
|
+
<div class="flex-1 border-t border-[#2a2a2a]"></div>
|
|
561
|
+
<span class="text-xs text-[#555]">${formatTimeDivider(root.createdAt)}</span>
|
|
562
|
+
<div class="flex-1 border-t border-[#2a2a2a]"></div>
|
|
563
|
+
</div>
|
|
564
|
+
`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (replies.length === 0) {
|
|
568
|
+
items.push(renderMessage(root, showHeader, !showHeader));
|
|
569
|
+
} else {
|
|
570
|
+
const isCollapsed = collapsedThreads.has(root.id);
|
|
571
|
+
const replyWord = replies.length === 1 ? "reply" : "replies";
|
|
572
|
+
const lastReply = replies[replies.length - 1];
|
|
573
|
+
items.push(html`
|
|
574
|
+
<div key=${root.id}>
|
|
575
|
+
${renderMessage(root, showHeader, !showHeader)}
|
|
576
|
+
<div class="ml-3 mb-2">
|
|
577
|
+
<button
|
|
578
|
+
class="text-xs text-[#666] hover:text-[#999] flex items-center gap-1 mb-1 bg-transparent border-none cursor-pointer p-0"
|
|
579
|
+
onClick=${() => handleThreadToggle(root.id)}
|
|
580
|
+
>
|
|
581
|
+
${isCollapsed ? "\u25B6" : "\u25BC"} ${replies.length} ${replyWord}
|
|
582
|
+
${
|
|
583
|
+
lastReply
|
|
584
|
+
? html`<span class="text-[#444] ml-1">· ${timeAgo(lastReply.createdAt)}</span>`
|
|
585
|
+
: null
|
|
586
|
+
}
|
|
587
|
+
</button>
|
|
588
|
+
${!isCollapsed && replies.map((reply) => renderMessage(reply, true, false))}
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return items;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// ----- Agent color helpers for sidebar and header -----
|
|
598
|
+
|
|
599
|
+
// Get capability colors for an agent (safe fallback for missing capability)
|
|
600
|
+
function capabilityColors(capability) {
|
|
601
|
+
return agentColor(capability || null);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return html`
|
|
605
|
+
<div class="flex h-full">
|
|
606
|
+
|
|
607
|
+
<!-- Sidebar -->
|
|
608
|
+
<div
|
|
609
|
+
class="w-64 bg-[#0f0f0f] border-r border-[#2a2a2a] overflow-y-auto flex-shrink-0"
|
|
610
|
+
>
|
|
611
|
+
<!-- All Messages item -->
|
|
612
|
+
<div
|
|
613
|
+
class=${
|
|
614
|
+
"px-3 py-2 cursor-pointer hover:bg-[#1a1a1a] flex items-center gap-2 text-sm" +
|
|
615
|
+
(!selectedTask && !selectedAgent && !selectedConversation
|
|
616
|
+
? " bg-[#1a1a1a] border-l-2 border-[#E64415]"
|
|
617
|
+
: "")
|
|
618
|
+
}
|
|
619
|
+
onClick=${handleAllMessagesClick}
|
|
620
|
+
>
|
|
621
|
+
<span class="text-[#e5e5e5]">All Messages</span>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<!-- Conversations section -->
|
|
625
|
+
<div class="mt-1 border-t border-[#1a1a1a]">
|
|
626
|
+
<div class="px-3 py-1 text-xs text-[#555] uppercase tracking-wider">
|
|
627
|
+
Conversations
|
|
628
|
+
</div>
|
|
629
|
+
${conversationEntries.map((conv) => {
|
|
630
|
+
const isSelected =
|
|
631
|
+
selectedConversation &&
|
|
632
|
+
selectedConversation.type === conv.type &&
|
|
633
|
+
selectedConversation.name === conv.name;
|
|
634
|
+
const colors = capabilityColors(conv.capability || null);
|
|
635
|
+
const convItemClass =
|
|
636
|
+
"px-3 py-2 cursor-pointer hover:bg-[#1a1a1a] flex items-center gap-2 text-sm" +
|
|
637
|
+
(isSelected ? " bg-[#1a1a1a] border-l-2 border-[#E64415]" : "");
|
|
638
|
+
return html`
|
|
639
|
+
<div
|
|
640
|
+
key=${conv.name}
|
|
641
|
+
class=${convItemClass}
|
|
642
|
+
onClick=${() => handleConversationClick(conv)}
|
|
643
|
+
>
|
|
644
|
+
<span class=${`text-xs shrink-0 ${colors.dot}`}>\u25CF</span>
|
|
645
|
+
<span class="flex-1 truncate text-[#e5e5e5] text-xs">${conv.label}</span>
|
|
646
|
+
${
|
|
647
|
+
conv.capability
|
|
648
|
+
? html`<span
|
|
649
|
+
class=${`text-xs px-1 rounded shrink-0 ${colors.bg} ${colors.text}`}
|
|
650
|
+
>${conv.capability}</span>`
|
|
651
|
+
: null
|
|
652
|
+
}
|
|
653
|
+
${
|
|
654
|
+
conv.state
|
|
655
|
+
? html`<span class="text-[#555] text-xs shrink-0">${conv.state}</span>`
|
|
656
|
+
: null
|
|
657
|
+
}
|
|
658
|
+
</div>
|
|
659
|
+
`;
|
|
660
|
+
})}
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<!-- Tasks section header -->
|
|
664
|
+
${
|
|
665
|
+
sortedGroups.length > 0 &&
|
|
666
|
+
html`<div
|
|
667
|
+
class="px-3 py-1 text-xs text-[#555] uppercase tracking-wider border-b border-[#1a1a1a] mt-1"
|
|
668
|
+
>
|
|
669
|
+
Tasks
|
|
670
|
+
</div>`
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
<!-- Task list -->
|
|
674
|
+
${sortedGroups.map(([beadId, group]) => {
|
|
675
|
+
const isTaskSelected = selectedTask === beadId && !selectedAgent && !selectedConversation;
|
|
676
|
+
const isExpanded = expandedTasks.has(beadId);
|
|
677
|
+
const status = group.issue?.status || "open";
|
|
678
|
+
const statusIcon = STATUS_ICONS[status] || STATUS_ICONS.open;
|
|
679
|
+
const statusColor = STATUS_ICON_COLORS[status] || STATUS_ICON_COLORS.open;
|
|
680
|
+
const rawTitle = group.issue?.title;
|
|
681
|
+
const title = rawTitle
|
|
682
|
+
? rawTitle.length > 32
|
|
683
|
+
? `${rawTitle.slice(0, 32)}\u2026`
|
|
684
|
+
: rawTitle
|
|
685
|
+
: beadId;
|
|
686
|
+
const taskItemClass =
|
|
687
|
+
"px-3 py-2 cursor-pointer hover:bg-[#1a1a1a] flex items-center gap-1.5 text-sm" +
|
|
688
|
+
(isTaskSelected ? " bg-[#1a1a1a] border-l-2 border-[#E64415]" : "");
|
|
689
|
+
|
|
690
|
+
// Primary agent capability color for the task row dot
|
|
691
|
+
const primaryAgent = group.agents[0];
|
|
692
|
+
const primaryColors = capabilityColors(primaryAgent?.capability);
|
|
693
|
+
|
|
694
|
+
return html`
|
|
695
|
+
<div key=${beadId}>
|
|
696
|
+
<div class=${taskItemClass} onClick=${() => handleTaskClick(beadId)}>
|
|
697
|
+
<button
|
|
698
|
+
class="text-xs text-[#555] hover:text-[#999] bg-transparent border-none cursor-pointer p-0 shrink-0"
|
|
699
|
+
onClick=${(e) => handleTaskToggle(e, beadId)}
|
|
700
|
+
>
|
|
701
|
+
${isExpanded ? "\u25BC" : "\u25B6"}
|
|
702
|
+
</button>
|
|
703
|
+
<span class=${`text-xs shrink-0 ${statusColor}`}>${statusIcon}</span>
|
|
704
|
+
<span class="flex-1 truncate text-[#e5e5e5] text-xs">${title}</span>
|
|
705
|
+
${
|
|
706
|
+
primaryAgent
|
|
707
|
+
? html`<span class=${`text-xs shrink-0 ${primaryColors.dot}`}>\u25CF</span>`
|
|
708
|
+
: null
|
|
709
|
+
}
|
|
710
|
+
${
|
|
711
|
+
group.unreadCount > 0
|
|
712
|
+
? html`<span
|
|
713
|
+
class="text-xs bg-[#E64415] text-white px-1 rounded-full shrink-0"
|
|
714
|
+
>${group.unreadCount}</span
|
|
715
|
+
>`
|
|
716
|
+
: group.msgCount > 0
|
|
717
|
+
? html`<span class="text-xs text-[#555] shrink-0"
|
|
718
|
+
>${group.msgCount}</span
|
|
719
|
+
>`
|
|
720
|
+
: null
|
|
721
|
+
}
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
${
|
|
725
|
+
isExpanded &&
|
|
726
|
+
group.agents.map((ag) => {
|
|
727
|
+
const isAgentSelected = selectedAgent === ag.agentName && !selectedConversation;
|
|
728
|
+
const colors = capabilityColors(ag.capability);
|
|
729
|
+
const agentItemClass =
|
|
730
|
+
"pl-8 pr-3 py-1.5 cursor-pointer hover:bg-[#1a1a1a] flex items-center gap-1.5 text-xs" +
|
|
731
|
+
(isAgentSelected ? " bg-[#1a1a1a] border-l-2 border-[#E64415]" : "");
|
|
732
|
+
|
|
733
|
+
return html`
|
|
734
|
+
<div
|
|
735
|
+
key=${ag.agentName}
|
|
736
|
+
class=${agentItemClass}
|
|
737
|
+
onClick=${(e) => handleAgentClick(e, ag.agentName)}
|
|
738
|
+
>
|
|
739
|
+
<span class=${`text-xs ${colors.dot}`}>\u25CF</span>
|
|
740
|
+
<span class="flex-1 truncate text-[#e5e5e5]">${ag.agentName}</span>
|
|
741
|
+
${
|
|
742
|
+
ag.capability
|
|
743
|
+
? html`<span
|
|
744
|
+
class=${`text-xs px-1 rounded shrink-0 ${colors.bg} ${colors.text}`}
|
|
745
|
+
>${ag.capability}</span>`
|
|
746
|
+
: null
|
|
747
|
+
}
|
|
748
|
+
<span class="text-[#555] shrink-0 ml-1">${ag.state || ""}</span>
|
|
749
|
+
</div>
|
|
750
|
+
`;
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
</div>
|
|
754
|
+
`;
|
|
755
|
+
})}
|
|
756
|
+
|
|
757
|
+
<!-- General / Unassigned section -->
|
|
758
|
+
${
|
|
759
|
+
showGeneral &&
|
|
760
|
+
html`<div class="mt-1 border-t border-[#1a1a1a]">
|
|
761
|
+
<div class="px-3 py-1 text-xs text-[#555] uppercase tracking-wider">
|
|
762
|
+
General
|
|
763
|
+
</div>
|
|
764
|
+
<div
|
|
765
|
+
class=${
|
|
766
|
+
"px-3 py-2 cursor-pointer hover:bg-[#1a1a1a] flex items-center gap-2 text-sm" +
|
|
767
|
+
(selectedTask === "__general__" && !selectedAgent && !selectedConversation
|
|
768
|
+
? " bg-[#1a1a1a] border-l-2 border-[#E64415]"
|
|
769
|
+
: "")
|
|
770
|
+
}
|
|
771
|
+
onClick=${() => handleTaskClick("__general__")}
|
|
772
|
+
>
|
|
773
|
+
<span class="flex-1 truncate text-[#e5e5e5] text-xs">Unassigned</span>
|
|
774
|
+
${
|
|
775
|
+
generalCounts.unreadCount > 0
|
|
776
|
+
? html`<span
|
|
777
|
+
class="text-xs bg-[#E64415] text-white px-1 rounded-full shrink-0"
|
|
778
|
+
>${generalCounts.unreadCount}</span
|
|
779
|
+
>`
|
|
780
|
+
: generalCounts.msgCount > 0
|
|
781
|
+
? html`<span class="text-xs text-[#555] shrink-0"
|
|
782
|
+
>${generalCounts.msgCount}</span
|
|
783
|
+
>`
|
|
784
|
+
: null
|
|
785
|
+
}
|
|
786
|
+
</div>
|
|
787
|
+
</div>`
|
|
788
|
+
}
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
<!-- Chat main area -->
|
|
792
|
+
<div class="flex-1 flex flex-col min-w-0">
|
|
793
|
+
|
|
794
|
+
<!-- Header -->
|
|
795
|
+
<div class="px-4 py-3 border-b border-[#2a2a2a] flex items-center gap-2 flex-wrap">
|
|
796
|
+
${
|
|
797
|
+
selectedConversation
|
|
798
|
+
? html`
|
|
799
|
+
<div class="flex items-center gap-2 border-l-4 pl-2 border-[#E64415]">
|
|
800
|
+
<span class="font-semibold text-[#e5e5e5]">${selectedConversation.label}</span>
|
|
801
|
+
${
|
|
802
|
+
selectedConversation.capability &&
|
|
803
|
+
html`<span class=${
|
|
804
|
+
`text-xs px-1.5 py-0.5 rounded` +
|
|
805
|
+
` ${capabilityColors(selectedConversation.capability).bg}` +
|
|
806
|
+
` ${capabilityColors(selectedConversation.capability).text}`
|
|
807
|
+
}>
|
|
808
|
+
${selectedConversation.capability}
|
|
809
|
+
</span>`
|
|
810
|
+
}
|
|
811
|
+
${
|
|
812
|
+
selectedConversation.state &&
|
|
813
|
+
html`<span class="text-xs px-1.5 py-0.5 rounded bg-[#333] text-[#999]">
|
|
814
|
+
${selectedConversation.state}
|
|
815
|
+
</span>`
|
|
816
|
+
}
|
|
817
|
+
<span class="text-xs text-[#555]">conversation</span>
|
|
818
|
+
</div>
|
|
819
|
+
`
|
|
820
|
+
: selectedAgent
|
|
821
|
+
? html`
|
|
822
|
+
<div class=${
|
|
823
|
+
"flex items-center gap-2 border-l-4 pl-2" +
|
|
824
|
+
(selectedAgentData
|
|
825
|
+
? ` ${capabilityColors(selectedAgentData.capability).border}`
|
|
826
|
+
: "")
|
|
827
|
+
}>
|
|
828
|
+
<span class="font-semibold text-[#e5e5e5]">${selectedAgent}</span>
|
|
829
|
+
${
|
|
830
|
+
selectedAgentData &&
|
|
831
|
+
html`
|
|
832
|
+
<span class=${
|
|
833
|
+
`text-xs px-1.5 py-0.5 rounded` +
|
|
834
|
+
` ${capabilityColors(selectedAgentData.capability).bg}` +
|
|
835
|
+
` ${capabilityColors(selectedAgentData.capability).text}`
|
|
836
|
+
}>
|
|
837
|
+
${selectedAgentData.capability || ""}
|
|
838
|
+
</span>
|
|
839
|
+
<span class="text-xs px-1.5 py-0.5 rounded bg-[#333] text-[#999]">
|
|
840
|
+
${selectedAgentData.state || ""}
|
|
841
|
+
</span>
|
|
842
|
+
`
|
|
843
|
+
}
|
|
844
|
+
</div>
|
|
845
|
+
`
|
|
846
|
+
: selectedTask === "__general__"
|
|
847
|
+
? html`<span class="font-semibold text-[#e5e5e5]">Unassigned Messages</span>`
|
|
848
|
+
: selectedTaskData
|
|
849
|
+
? html`
|
|
850
|
+
<span class="font-mono text-xs text-[#999]">${selectedTask}</span>
|
|
851
|
+
<span class="font-semibold text-[#e5e5e5] truncate">
|
|
852
|
+
${selectedTaskData.issue?.title || selectedTask}
|
|
853
|
+
</span>
|
|
854
|
+
${
|
|
855
|
+
selectedTaskData.issue &&
|
|
856
|
+
html`<span
|
|
857
|
+
class=${
|
|
858
|
+
"text-xs px-1.5 py-0.5 rounded bg-[#333] " +
|
|
859
|
+
(STATUS_ICON_COLORS[selectedTaskData.issue.status] || "text-gray-400")
|
|
860
|
+
}
|
|
861
|
+
>
|
|
862
|
+
${selectedTaskData.issue.status}
|
|
863
|
+
</span>`
|
|
864
|
+
}
|
|
865
|
+
<span class="flex items-center gap-1">
|
|
866
|
+
${selectedTaskData.agents.map(
|
|
867
|
+
(ag) => html`<span
|
|
868
|
+
key=${ag.agentName}
|
|
869
|
+
class=${`text-xs ${capabilityColors(ag.capability).dot}`}
|
|
870
|
+
title=${ag.agentName}
|
|
871
|
+
>\u25CF</span>`,
|
|
872
|
+
)}
|
|
873
|
+
</span>
|
|
874
|
+
<span class="text-xs text-[#666]">
|
|
875
|
+
${selectedTaskData.agents.length}
|
|
876
|
+
${selectedTaskData.agents.length === 1 ? "agent" : "agents"}
|
|
877
|
+
</span>
|
|
878
|
+
`
|
|
879
|
+
: html`<span class="font-semibold text-[#e5e5e5]">All Messages</span>`
|
|
880
|
+
}
|
|
881
|
+
<span class="flex-1"></span>
|
|
882
|
+
${
|
|
883
|
+
unreadInView > 0 &&
|
|
884
|
+
html`<span class="text-xs bg-[#E64415] text-white px-1.5 py-0.5 rounded-full">
|
|
885
|
+
${unreadInView} unread
|
|
886
|
+
</span>`
|
|
887
|
+
}
|
|
888
|
+
<!-- Chat/All mode toggle (only shown when not in conversation mode) -->
|
|
889
|
+
${
|
|
890
|
+
!selectedConversation &&
|
|
891
|
+
html`<div class="flex items-center gap-0.5 bg-[#1a1a1a] border border-[#2a2a2a] rounded-full px-0.5 py-0.5">
|
|
892
|
+
<button
|
|
893
|
+
class=${
|
|
894
|
+
"text-xs px-2.5 py-0.5 rounded-full border-none cursor-pointer transition-colors" +
|
|
895
|
+
(chatMode === "chat"
|
|
896
|
+
? " bg-[#E64415] text-white"
|
|
897
|
+
: " bg-transparent text-[#666] hover:text-[#999]")
|
|
898
|
+
}
|
|
899
|
+
onClick=${() => setChatMode("chat")}
|
|
900
|
+
>Chat</button>
|
|
901
|
+
<button
|
|
902
|
+
class=${
|
|
903
|
+
"text-xs px-2.5 py-0.5 rounded-full border-none cursor-pointer transition-colors" +
|
|
904
|
+
(chatMode === "all"
|
|
905
|
+
? " bg-[#E64415] text-white"
|
|
906
|
+
: " bg-transparent text-[#666] hover:text-[#999]")
|
|
907
|
+
}
|
|
908
|
+
onClick=${() => setChatMode("all")}
|
|
909
|
+
>All</button>
|
|
910
|
+
</div>`
|
|
911
|
+
}
|
|
912
|
+
</div>
|
|
913
|
+
|
|
914
|
+
<!-- Message feed -->
|
|
915
|
+
<div
|
|
916
|
+
class="flex-1 overflow-y-auto p-4 min-h-0"
|
|
917
|
+
ref=${feedRef}
|
|
918
|
+
onScroll=${handleFeedScroll}
|
|
919
|
+
>
|
|
920
|
+
${
|
|
921
|
+
roots.length === 0
|
|
922
|
+
? selectedConversation
|
|
923
|
+
? html`<div class="flex flex-col items-center justify-center h-full text-[#666] text-sm gap-1">
|
|
924
|
+
<span>No messages yet.</span>
|
|
925
|
+
<span class="text-xs">Start a conversation below.</span>
|
|
926
|
+
</div>`
|
|
927
|
+
: chatMode === "chat"
|
|
928
|
+
? html`<div class="flex flex-col items-center justify-center h-full text-[#666] text-sm gap-1">
|
|
929
|
+
<span>No conversation messages yet.</span>
|
|
930
|
+
<span class="text-xs">Switch to "All" to see protocol activity.</span>
|
|
931
|
+
</div>`
|
|
932
|
+
: html`<div class="flex items-center justify-center h-full text-[#666] text-sm">No messages yet</div>`
|
|
933
|
+
: feedItems
|
|
934
|
+
}
|
|
935
|
+
</div>
|
|
936
|
+
|
|
937
|
+
<!-- Chat input (simplified: To + Body + Send) -->
|
|
938
|
+
<div class="border-t border-[#2a2a2a] p-3">
|
|
939
|
+
<div class="flex gap-2 items-end">
|
|
940
|
+
<div class="flex-1">
|
|
941
|
+
<div class="flex items-center gap-1 mb-1">
|
|
942
|
+
<span class="text-xs text-[#555]">To:</span>
|
|
943
|
+
${
|
|
944
|
+
selectedConversation
|
|
945
|
+
? html`<span class="text-xs text-[#e5e5e5] font-medium">${selectedConversation.label}</span>`
|
|
946
|
+
: html`<input
|
|
947
|
+
type="text"
|
|
948
|
+
value=${toVal}
|
|
949
|
+
onInput=${(e) => setToVal(e.target.value)}
|
|
950
|
+
class="bg-transparent border-none text-xs text-[#e5e5e5] outline-none w-24"
|
|
951
|
+
/>`
|
|
952
|
+
}
|
|
953
|
+
</div>
|
|
954
|
+
<textarea
|
|
955
|
+
placeholder="Message... (Ctrl+Enter or Cmd+Enter to send)"
|
|
956
|
+
rows="2"
|
|
957
|
+
value=${bodyVal}
|
|
958
|
+
onInput=${(e) => setBodyVal(e.target.value)}
|
|
959
|
+
onKeyDown=${(e) => {
|
|
960
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
if (!bodyVal.trim()) return;
|
|
963
|
+
handleSend();
|
|
964
|
+
}
|
|
965
|
+
}}
|
|
966
|
+
class=${`w-full ${inputClass} resize-none`}
|
|
967
|
+
/>
|
|
968
|
+
</div>
|
|
969
|
+
<button
|
|
970
|
+
onClick=${handleSend}
|
|
971
|
+
disabled=${sending || !bodyVal.trim()}
|
|
972
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none self-end"
|
|
973
|
+
>
|
|
974
|
+
${sending ? "..." : "Send"}
|
|
975
|
+
</button>
|
|
976
|
+
</div>
|
|
977
|
+
${sendError && html`<span class="text-xs text-red-400 mt-1 block">${sendError}</span>`}
|
|
978
|
+
</div>
|
|
979
|
+
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
`;
|
|
983
|
+
}
|