agent-social-mcp 0.2.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.
@@ -0,0 +1,866 @@
1
+ export const chatHtml = String.raw `<!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Agent Social</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ --bg: light-dark(#f4f1ea, #111513);
11
+ --panel: light-dark(#fffdf8, #18201d);
12
+ --panel-2: light-dark(#ebe6db, #202a26);
13
+ --ink: light-dark(#171917, #f3f7f4);
14
+ --muted: light-dark(#646961, #aeb9b2);
15
+ --line: light-dark(#d4cec1, #39463f);
16
+ --accent: #0f766e;
17
+ --accent-strong: #0b5f59;
18
+ --danger: #a53a35;
19
+ --sent: light-dark(#123a35, #0f766e);
20
+ --focus: #e49b0f;
21
+ --shadow: 0 14px 44px rgba(12, 25, 20, .12);
22
+ }
23
+ * { box-sizing: border-box; }
24
+ html, body { margin: 0; height: 100%; }
25
+ body {
26
+ background: var(--bg);
27
+ color: var(--ink);
28
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
29
+ overflow: hidden;
30
+ }
31
+ button, input, textarea { font: inherit; }
32
+ button, input, textarea { border-radius: 8px; }
33
+ button {
34
+ min-height: 40px;
35
+ border: 1px solid var(--ink);
36
+ background: var(--ink);
37
+ color: var(--panel);
38
+ padding: 9px 13px;
39
+ cursor: pointer;
40
+ }
41
+ button:hover { transform: translateY(-1px); }
42
+ button.secondary { background: transparent; color: var(--ink); border-color: var(--line); }
43
+ button.danger { background: transparent; color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, var(--line)); }
44
+ button:disabled { opacity: .46; cursor: not-allowed; transform: none; }
45
+ :focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
46
+ .app { display: grid; grid-template-columns: 270px minmax(360px, 1fr) 300px; height: 100dvh; }
47
+ .sidebar, main, .rail { min-height: 0; }
48
+ .sidebar {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: 14px;
52
+ padding: 18px 14px;
53
+ border-right: 1px solid var(--line);
54
+ background: var(--panel-2);
55
+ }
56
+ .brandRow, .topbar, .actionRow, .composerActions, .assistanceHeader, .toggleRow, .assistanceActions, .inlineChoice { display: flex; align-items: center; gap: 8px; }
57
+ .brandRow { justify-content: space-between; }
58
+ h1 { font-size: 19px; margin: 0; }
59
+ h2 { font-size: 14px; margin: 0 0 10px; }
60
+ .muted, .status { color: var(--muted); font-size: 12px; line-height: 1.45; }
61
+ .status[data-online="true"] { color: var(--accent); }
62
+ .setup {
63
+ display: grid;
64
+ gap: 8px;
65
+ padding: 12px;
66
+ border: 1px solid var(--line);
67
+ background: color-mix(in srgb, var(--panel) 68%, transparent);
68
+ }
69
+ .setup[hidden] { display: none; }
70
+ label { font-size: 13px; font-weight: 700; }
71
+ input, textarea {
72
+ width: 100%;
73
+ border: 1px solid var(--line);
74
+ background: var(--panel);
75
+ color: var(--ink);
76
+ padding: 10px 11px;
77
+ }
78
+ .sessionList { display: flex; flex-direction: column; gap: 8px; overflow: auto; min-height: 0; }
79
+ .session { text-align: left; color: var(--ink); background: color-mix(in srgb, var(--panel) 54%, transparent); border-color: var(--line); }
80
+ .session.active { background: var(--panel); border-color: var(--ink); box-shadow: var(--shadow); }
81
+ .sessionTop { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-weight: 750; }
82
+ .preview { margin-top: 6px; color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
83
+ .badge { display: inline-grid; place-items: center; min-width: 22px; height: 22px; padding: 0 5px; border-radius: 999px; background: var(--accent); color: white; font-size: 12px; }
84
+ main { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; background: var(--bg); }
85
+ .topbar { min-height: 74px; justify-content: space-between; padding: 12px 18px; border-bottom: 1px solid var(--line); }
86
+ .sessionHeading { min-width: 0; }
87
+ .sessionHeading strong, .sessionHeading span { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
88
+ .actionRow { flex-wrap: wrap; justify-content: flex-end; }
89
+ .actionRow button { min-height: 34px; padding: 6px 9px; font-size: 12px; }
90
+ .messages { overflow: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
91
+ .empty { margin: auto; max-width: 420px; text-align: center; color: var(--muted); line-height: 1.6; }
92
+ .bubble { max-width: min(620px, 82%); padding: 12px 14px; border: 1px solid var(--line); background: var(--panel); align-self: flex-start; box-shadow: 0 8px 28px rgba(20, 30, 25, .06); }
93
+ .bubble.sent { align-self: flex-end; background: var(--sent); border-color: var(--sent); color: white; }
94
+ .messageMeta { margin-top: 7px; font-size: 11px; color: var(--muted); }
95
+ .bubble.sent .messageMeta { color: rgba(255,255,255,.7); }
96
+ .expired { font-style: italic; color: var(--muted); }
97
+ .composer { padding: 12px 16px; border-top: 1px solid var(--line); background: var(--panel-2); }
98
+ .composer textarea { min-height: 74px; resize: vertical; line-height: 1.45; }
99
+ .composerActions { justify-content: flex-end; margin-top: 8px; }
100
+ .rail { overflow: auto; padding: 18px; border-left: 1px solid var(--line); background: var(--panel); }
101
+ .railSection { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
102
+ .assistanceHeader { justify-content: space-between; align-items: flex-start; }
103
+ .toggleRow { font-size: 12px; font-weight: 650; color: var(--muted); }
104
+ .toggleRow input { width: auto; accent-color: var(--accent); }
105
+ .assistanceCard { margin-top: 12px; padding: 12px; border: 1px solid var(--line); background: var(--panel-2); border-radius: 10px; }
106
+ .assistanceCard p { margin: 0; line-height: 1.55; }
107
+ .assistanceAction { margin-top: 10px; font-size: 12px; font-weight: 750; color: var(--accent); }
108
+ .suggestionList { display: grid; gap: 8px; margin-top: 12px; }
109
+ .suggestion { width: 100%; text-align: left; background: var(--panel); color: var(--ink); border-color: var(--line); line-height: 1.45; }
110
+ .assistanceActions { margin-top: 12px; flex-wrap: wrap; }
111
+ .assistanceActions button, .inlineChoice button { min-height: 34px; padding: 6px 9px; font-size: 12px; }
112
+ .inlineChoice { align-items: stretch; flex-direction: column; margin-top: 12px; padding: 10px; border: 1px solid var(--focus); border-radius: 8px; }
113
+ .inlineChoice[hidden] { display: none; }
114
+ .inlineChoice .choiceButtons { display: flex; gap: 6px; flex-wrap: wrap; }
115
+ ul { margin: 8px 0 0; padding-left: 19px; color: var(--muted); font-size: 13px; line-height: 1.55; }
116
+ .toast { position: fixed; right: 18px; bottom: 18px; z-index: 4; padding: 11px 14px; background: var(--ink); color: var(--panel); box-shadow: var(--shadow); border-radius: 8px; }
117
+ .toast[hidden] { display: none; }
118
+ dialog { width: min(440px, calc(100% - 32px)); border: 1px solid var(--line); background: var(--panel); color: var(--ink); box-shadow: var(--shadow); padding: 0; border-radius: 12px; }
119
+ dialog::backdrop { background: rgba(4, 10, 7, .58); }
120
+ .dialogBody { padding: 20px; }
121
+ .dialogActions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
122
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
123
+ @media (max-width: 900px) {
124
+ .app { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr) auto; }
125
+ .sidebar { padding: 10px 14px; border-right: 0; border-bottom: 1px solid var(--line); gap: 9px; }
126
+ .setup { grid-template-columns: minmax(0, 1fr) auto; align-items: end; padding: 8px; }
127
+ .setup label { grid-column: 1 / -1; }
128
+ .sessionList { flex-direction: row; overflow-x: auto; overflow-y: hidden; }
129
+ .session { min-width: 190px; }
130
+ .rail { max-height: 210px; border-left: 0; border-top: 1px solid var(--line); padding: 14px 18px; }
131
+ .topbar { min-height: 66px; padding: 9px 14px; }
132
+ .messages { padding: 14px; }
133
+ }
134
+ @media (max-width: 560px) {
135
+ .topbar { align-items: flex-start; }
136
+ .actionRow { max-width: 180px; }
137
+ .actionRow button span { display: none; }
138
+ .bubble { max-width: 92%; }
139
+ .composerActions { justify-content: stretch; }
140
+ .composerActions button { flex: 1; }
141
+ }
142
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } }
143
+ </style>
144
+ </head>
145
+ <body>
146
+ <div class="app">
147
+ <aside class="sidebar" aria-label="Conversations">
148
+ <div class="brandRow">
149
+ <div>
150
+ <h1>Agent Social</h1>
151
+ <div id="connectionStatus" class="status" role="status" aria-live="polite">Connecting…</div>
152
+ </div>
153
+ <button id="syncNow" class="secondary" type="button">Sync</button>
154
+ </div>
155
+ <form id="setupForm" class="setup">
156
+ <label for="credentialInput">Anonymous access token</label>
157
+ <input id="credentialInput" name="credential" type="password" autocomplete="off" placeholder="Paste once; it stays in the local process">
158
+ <button type="submit">Save and connect</button>
159
+ </form>
160
+ <div id="setupHint" class="muted">Ask your Agent to onboard or paste your token once.</div>
161
+ <div id="sessionList" class="sessionList" aria-label="Match sessions"></div>
162
+ </aside>
163
+
164
+ <main>
165
+ <header class="topbar">
166
+ <div class="sessionHeading">
167
+ <strong id="sessionTitle">No session selected</strong>
168
+ <span id="sessionSub" class="status">Ask your Agent to find a match.</span>
169
+ </div>
170
+ <div class="actionRow" aria-label="Session safety actions">
171
+ <button id="enableNotifications" class="secondary" type="button">Enable alerts</button>
172
+ <button id="reportSession" class="danger" type="button" disabled>Report</button>
173
+ <button id="blockSession" class="danger" type="button" disabled>Block</button>
174
+ <button id="closeSession" class="secondary" type="button" disabled>Close</button>
175
+ </div>
176
+ </header>
177
+ <div id="messages" class="messages" role="log" aria-live="polite" aria-relevant="additions text">
178
+ <div class="empty">Your messages will appear here when a match is active.</div>
179
+ </div>
180
+ <form id="composer" class="composer">
181
+ <label class="srOnly" for="messageInput">Message</label>
182
+ <textarea id="messageInput" placeholder="Write a message. Shift + Enter sends; Enter adds a new line." disabled></textarea>
183
+ <div class="composerActions">
184
+ <button id="sendMessage" type="submit" disabled>Send</button>
185
+ </div>
186
+ </form>
187
+ </main>
188
+
189
+ <section class="rail" aria-labelledby="assistanceHeading">
190
+ <div class="assistanceHeader">
191
+ <h2 id="assistanceHeading">Agent assistance</h2>
192
+ <label class="toggleRow" for="autoAnalyze">
193
+ <input id="autoAnalyze" type="checkbox" checked>
194
+ Auto-analyze
195
+ </label>
196
+ </div>
197
+ <div id="agentAssistance" class="muted">Select a conversation to see Agent assistance.</div>
198
+ <div id="assistanceActions" class="assistanceActions">
199
+ <button id="analyzeReply" class="secondary" type="button" disabled>Analyze with Agent</button>
200
+ </div>
201
+ <div id="suggestionChoice" class="inlineChoice" hidden>
202
+ <span id="suggestionPreview" class="muted"></span>
203
+ <div class="choiceButtons">
204
+ <button id="replaceSuggestion" type="button">Replace</button>
205
+ <button id="appendSuggestion" class="secondary" type="button">Append</button>
206
+ <button id="cancelSuggestion" class="secondary" type="button">Cancel</button>
207
+ </div>
208
+ </div>
209
+ <section class="railSection" aria-labelledby="replyHeading">
210
+ <h2 id="replyHeading">Safety context</h2>
211
+ <div id="replyContext" class="muted">Select a conversation to see the latest context.</div>
212
+ </section>
213
+ </section>
214
+ </div>
215
+
216
+ <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
217
+ <dialog id="confirmDialog" aria-labelledby="confirmTitle">
218
+ <form method="dialog" class="dialogBody">
219
+ <h2 id="confirmTitle">Confirm action</h2>
220
+ <p id="confirmText" class="muted"></p>
221
+ <div id="reportReasonWrap" hidden>
222
+ <label for="reportReason">Reason</label>
223
+ <textarea id="reportReason" placeholder="Briefly describe the safety issue"></textarea>
224
+ </div>
225
+ <div class="dialogActions">
226
+ <button value="cancel" class="secondary">Cancel</button>
227
+ <button id="confirmAction" value="confirm" class="danger">Confirm</button>
228
+ </div>
229
+ </form>
230
+ </dialog>
231
+
232
+ <script>
233
+ const embedded = window.parent !== window;
234
+ const state = {
235
+ connected: false,
236
+ sessions: [],
237
+ messages: [],
238
+ selectedSessionId: "",
239
+ preferences: { autoAnalyze: true },
240
+ analysisTargets: [],
241
+ replyAssistance: []
242
+ };
243
+ const els = Object.fromEntries([
244
+ "connectionStatus", "syncNow", "setupForm", "setupHint", "credentialInput", "sessionList",
245
+ "sessionTitle", "sessionSub", "enableNotifications", "reportSession", "blockSession",
246
+ "closeSession", "messages", "composer", "messageInput", "sendMessage", "autoAnalyze",
247
+ "agentAssistance", "assistanceActions", "analyzeReply", "suggestionChoice", "suggestionPreview",
248
+ "replaceSuggestion", "appendSuggestion", "cancelSuggestion", "replyContext", "toast",
249
+ "confirmDialog", "confirmTitle", "confirmText", "reportReasonWrap", "reportReason", "confirmAction"
250
+ ].map((id) => [id, document.getElementById(id)]));
251
+ let pendingAction = "";
252
+ let rpcId = 1;
253
+ const pendingRpc = new Map();
254
+ let embeddedRetryMs = 1000;
255
+ let toastTimer;
256
+ let fallbackAlertsEnabled = false;
257
+ let alertAudioContext;
258
+ const baseTitle = document.title;
259
+ const analysisOutcomes = new Map();
260
+ let analysisTimer;
261
+ let analysisTimerHash = "";
262
+ let analysisInFlightHash = "";
263
+ let pendingSuggestion = "";
264
+ let sending = false;
265
+
266
+ window.addEventListener("message", (event) => {
267
+ const message = event.data;
268
+ if (!message || message.jsonrpc !== "2.0") return;
269
+ if (message.id && pendingRpc.has(message.id)) {
270
+ const pending = pendingRpc.get(message.id);
271
+ pendingRpc.delete(message.id);
272
+ message.error ? pending.reject(new Error(message.error.message || "Host request failed")) : pending.resolve(message.result);
273
+ return;
274
+ }
275
+ if (message.method === "ui/notifications/tool-result") applyPayload(normalizeToolResult(message.params));
276
+ if (message.method === "ui/notifications/host-context-changed") applyHostContext(message.params);
277
+ });
278
+
279
+ function sendRequest(method, params) {
280
+ const id = rpcId++;
281
+ window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*");
282
+ return new Promise((resolve, reject) => pendingRpc.set(id, { resolve, reject }));
283
+ }
284
+ function sendNotification(method, params) {
285
+ window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
286
+ }
287
+ function normalizeToolResult(result) {
288
+ return result && result.structuredContent ? result.structuredContent : result || {};
289
+ }
290
+ async function callEmbeddedTool(name, args) {
291
+ if (window.openai && typeof window.openai.callTool === "function") {
292
+ return normalizeToolResult(await window.openai.callTool(name, args));
293
+ }
294
+ return normalizeToolResult(await sendRequest("tools/call", { name, arguments: args || {} }));
295
+ }
296
+ async function api(path, options) {
297
+ const config = options || {};
298
+ const response = await fetch(path, {
299
+ method: config.method || "GET",
300
+ headers: { "content-type": "application/json" },
301
+ body: config.body ? JSON.stringify(config.body) : undefined
302
+ });
303
+ const payload = await response.json();
304
+ if (!response.ok || payload.ok === false) throw new Error(payload.error || "Request failed");
305
+ return payload;
306
+ }
307
+ async function action(name, args) {
308
+ if (embedded) return callEmbeddedTool(name, args);
309
+ const routes = {
310
+ dating_ui_state: ["/api/state", "GET"],
311
+ dating_ui_sync: ["/api/messages/sync", "POST"],
312
+ dating_ui_send: ["/api/messages/send", "POST"],
313
+ dating_ui_read: ["/api/sessions/read", "POST"],
314
+ dating_ui_report: ["/api/sessions/report", "POST"],
315
+ dating_ui_block: ["/api/sessions/block", "POST"],
316
+ dating_ui_close: ["/api/sessions/close", "POST"],
317
+ dating_ui_preferences: ["/api/preferences", "POST"]
318
+ };
319
+ const route = routes[name];
320
+ if (!route) throw new Error("This action requires an Agent host");
321
+ return api(route[0], { method: route[1], body: args || {} });
322
+ }
323
+
324
+ function applyPayload(payload) {
325
+ if (!payload || payload.ok === false) return;
326
+ const oldInbound = new Set(state.messages.filter((m) => m.direction === "received").map((m) => m.id));
327
+ state.connected = payload.connected !== undefined ? Boolean(payload.connected) : state.connected;
328
+ state.sessions = payload.sessions || state.sessions;
329
+ state.messages = payload.messages || state.messages;
330
+ state.preferences = payload.preferences || state.preferences;
331
+ state.analysisTargets = payload.analysisTargets || state.analysisTargets;
332
+ state.replyAssistance = payload.replyAssistance || state.replyAssistance;
333
+ if (!state.selectedSessionId || !state.sessions.some((s) => s.id === state.selectedSessionId)) {
334
+ const firstActive = state.sessions.find((s) => s.status === "active");
335
+ state.selectedSessionId = firstActive ? firstActive.id : (state.sessions[0] ? state.sessions[0].id : "");
336
+ }
337
+ render();
338
+ setTimeout(maybeScheduleAutoAnalysis, 0);
339
+ const fresh = state.messages.find((m) => m.direction === "received" && !oldInbound.has(m.id));
340
+ if (fresh && document.hidden) notifyIncomingMessage();
341
+ }
342
+
343
+ function render() {
344
+ els.setupForm.hidden = embedded || state.connected;
345
+ els.setupHint.hidden = state.connected;
346
+ els.connectionStatus.textContent = state.connected ? "Connected" : "Not connected";
347
+ els.connectionStatus.dataset.online = String(state.connected);
348
+ els.sessionList.replaceChildren();
349
+ if (!state.sessions.length) {
350
+ const empty = document.createElement("div");
351
+ empty.className = "muted";
352
+ empty.textContent = state.connected ? "No sessions yet. Ask your Agent to find a match." : "Connect to load sessions.";
353
+ els.sessionList.appendChild(empty);
354
+ }
355
+ for (const session of state.sessions) {
356
+ const button = document.createElement("button");
357
+ button.type = "button";
358
+ button.className = "session" + (session.id === state.selectedSessionId ? " active" : "");
359
+ button.setAttribute("aria-pressed", String(session.id === state.selectedSessionId));
360
+ const top = document.createElement("div");
361
+ top.className = "sessionTop";
362
+ const name = document.createElement("span");
363
+ name.textContent = session.label || session.peerId || "Anonymous match";
364
+ const marker = document.createElement("span");
365
+ marker.className = session.unreadCount ? "badge" : "status";
366
+ marker.textContent = session.unreadCount ? String(session.unreadCount) : session.status;
367
+ top.append(name, marker);
368
+ const preview = document.createElement("div");
369
+ preview.className = "preview";
370
+ preview.textContent = session.lastMessagePreview || session.peerIntro || "No messages yet";
371
+ button.append(top, preview);
372
+ button.addEventListener("click", () => selectSession(session.id));
373
+ els.sessionList.appendChild(button);
374
+ }
375
+ const selected = selectedSession();
376
+ const active = Boolean(selected && selected.status === "active");
377
+ els.sessionTitle.textContent = selected ? (selected.label || selected.peerId) : "No session selected";
378
+ els.sessionSub.textContent = selected ? (selected.peerIntro || selected.status) : "Ask your Agent to find a match.";
379
+ for (const button of [els.reportSession, els.blockSession, els.closeSession]) button.disabled = !active;
380
+ els.sendMessage.disabled = !active || sending;
381
+ els.messageInput.disabled = !active;
382
+ els.autoAnalyze.checked = state.preferences.autoAnalyze !== false;
383
+ els.autoAnalyze.disabled = !state.connected;
384
+ const messages = selected ? state.messages.filter((message) => message.sessionId === selected.id) : [];
385
+ els.messages.replaceChildren();
386
+ if (!messages.length) {
387
+ const empty = document.createElement("div");
388
+ empty.className = "empty";
389
+ empty.textContent = selected ? "No messages yet. Start with something specific and low-pressure." : "Your messages will appear here when a match is active.";
390
+ els.messages.appendChild(empty);
391
+ }
392
+ for (const message of messages) {
393
+ const item = document.createElement("article");
394
+ item.className = "bubble " + message.direction;
395
+ const body = document.createElement("div");
396
+ body.textContent = message.body || "Message expired after the safety window.";
397
+ if (!message.body) body.className = "expired";
398
+ const meta = document.createElement("div");
399
+ meta.className = "messageMeta";
400
+ meta.textContent = (message.direction === "sent" ? "You" : "Them") + " · " +
401
+ new Date(message.createdAt).toLocaleTimeString() + " · " + deliveryLabel(message);
402
+ item.append(body, meta);
403
+ els.messages.appendChild(item);
404
+ }
405
+ els.messages.scrollTop = els.messages.scrollHeight;
406
+ renderReplyContext(messages);
407
+ renderAgentAssistance();
408
+ }
409
+
410
+ function renderAgentAssistance() {
411
+ const target = currentAnalysisTarget();
412
+ const outcome = target ? analysisOutcomes.get(target.contextHash) : undefined;
413
+ const assistance = target
414
+ ? state.replyAssistance.find((candidate) =>
415
+ candidate.sessionId === target.sessionId && candidate.contextHash === target.contextHash)
416
+ : undefined;
417
+ els.agentAssistance.replaceChildren();
418
+ els.analyzeReply.hidden = false;
419
+ els.analyzeReply.disabled = !target || !embedded || analysisInFlightHash === (target && target.contextHash);
420
+ els.analyzeReply.textContent = assistance ? "Analyze again" : "Analyze with Agent";
421
+
422
+ if (!target) {
423
+ appendAssistanceText("No inbound message is waiting for analysis.");
424
+ if (!embedded) els.analyzeReply.textContent = "Agent host required";
425
+ return;
426
+ }
427
+ if (assistance) {
428
+ const card = document.createElement("div");
429
+ card.className = "assistanceCard";
430
+ const analysis = document.createElement("p");
431
+ analysis.textContent = assistance.analysis;
432
+ const action = document.createElement("div");
433
+ action.className = "assistanceAction";
434
+ action.textContent = "Recommended action: " + assistance.recommendedAction;
435
+ card.append(analysis, action);
436
+ if (assistance.suggestions && assistance.suggestions.length) {
437
+ const list = document.createElement("div");
438
+ list.className = "suggestionList";
439
+ for (const suggestion of assistance.suggestions) {
440
+ const button = document.createElement("button");
441
+ button.type = "button";
442
+ button.className = "suggestion";
443
+ button.textContent = suggestion;
444
+ button.addEventListener("click", () => selectSuggestion(suggestion));
445
+ list.appendChild(button);
446
+ }
447
+ card.appendChild(list);
448
+ }
449
+ els.agentAssistance.appendChild(card);
450
+ return;
451
+ }
452
+ if (!state.preferences.autoAnalyze) {
453
+ appendAssistanceText("Auto-analysis is off. Analyze manually when you want Agent help.");
454
+ return;
455
+ }
456
+ if (target.autoEligible === false && !outcome) {
457
+ appendAssistanceText("This looks like a low-information reply, so the Agent call was skipped.");
458
+ return;
459
+ }
460
+ if (outcome?.status === "fallback") {
461
+ appendAssistanceText("Background sampling is unavailable in this host. Ask your Agent in the main conversation.");
462
+ els.analyzeReply.textContent = "Ask my Agent";
463
+ els.analyzeReply.disabled = false;
464
+ return;
465
+ }
466
+ if (analysisInFlightHash === target.contextHash || outcome?.status === "analyzing" || outcome?.status === "fallback-sent") {
467
+ appendAssistanceText(outcome?.status === "fallback-sent" ? "Waiting for your Agent response…" : "Agent is analyzing this conversation…");
468
+ els.analyzeReply.disabled = true;
469
+ return;
470
+ }
471
+ if (target.status === "analyzing") {
472
+ appendAssistanceText("A previous Agent request is still waiting. You can analyze again if it does not complete.");
473
+ els.analyzeReply.textContent = "Analyze again";
474
+ els.analyzeReply.disabled = false;
475
+ return;
476
+ }
477
+ if (outcome?.status === "failed") {
478
+ appendAssistanceText("Agent analysis failed. Your conversation is still available and you can try again.");
479
+ return;
480
+ }
481
+ if (target.status === "stale") {
482
+ appendAssistanceText("A newer message made the previous Agent analysis stale.");
483
+ return;
484
+ }
485
+ appendAssistanceText(embedded
486
+ ? "Agent assistance is ready for this conversation."
487
+ : "Open this conversation in an MCP App host to request Agent analysis.");
488
+ if (!embedded) els.analyzeReply.textContent = "Agent host required";
489
+ }
490
+
491
+ function appendAssistanceText(text) {
492
+ const paragraph = document.createElement("p");
493
+ paragraph.className = "muted";
494
+ paragraph.textContent = text;
495
+ els.agentAssistance.appendChild(paragraph);
496
+ }
497
+
498
+ function currentAnalysisTarget() {
499
+ return state.analysisTargets.find((target) => target.sessionId === state.selectedSessionId);
500
+ }
501
+
502
+ function renderReplyContext(messages) {
503
+ const inbound = [...messages].reverse().find((message) => message.direction === "received" && message.replyContext);
504
+ els.replyContext.replaceChildren();
505
+ if (!inbound) {
506
+ els.replyContext.textContent = "No reply context yet.";
507
+ return;
508
+ }
509
+ const signal = document.createElement("p");
510
+ signal.className = "muted";
511
+ signal.textContent = inbound.replyContext.inferredSignal || "Read the message in context before replying.";
512
+ els.replyContext.appendChild(signal);
513
+ appendList("Suggested reply angles", inbound.replyContext.suggestedAngles || []);
514
+ appendList("Safety reminders", inbound.replyContext.safetyReminders || []);
515
+ }
516
+ function appendList(title, items) {
517
+ const section = document.createElement("section");
518
+ section.className = "railSection";
519
+ const heading = document.createElement("h2");
520
+ heading.textContent = title;
521
+ const list = document.createElement("ul");
522
+ for (const value of items) { const item = document.createElement("li"); item.textContent = value; list.appendChild(item); }
523
+ section.append(heading, list);
524
+ els.replyContext.appendChild(section);
525
+ }
526
+ function selectedSession() { return state.sessions.find((session) => session.id === state.selectedSessionId); }
527
+ function deliveryLabel(message) {
528
+ return ({ queued: "Queued", delivered: "Delivered", read: "Read", rejected: "Rejected" })[message.deliveryState] || "Pending";
529
+ }
530
+ async function selectSession(sessionId) {
531
+ state.selectedSessionId = sessionId;
532
+ pendingSuggestion = "";
533
+ els.suggestionChoice.hidden = true;
534
+ render();
535
+ try { applyPayload(await action("dating_ui_read", { sessionId })); } catch (error) { showError(error); }
536
+ }
537
+ function showToast(text) {
538
+ clearTimeout(toastTimer);
539
+ els.toast.textContent = text;
540
+ els.toast.hidden = false;
541
+ toastTimer = setTimeout(() => { els.toast.hidden = true; }, 2200);
542
+ }
543
+ function showError(error) {
544
+ els.connectionStatus.textContent = "Reconnecting — " + (error && error.message ? error.message : "sync failed");
545
+ els.connectionStatus.dataset.online = "false";
546
+ }
547
+
548
+ function updateAlertControl() {
549
+ if (window.Notification && Notification.permission === "granted") {
550
+ els.enableNotifications.textContent = "Test notification";
551
+ } else if (!window.Notification && fallbackAlertsEnabled) {
552
+ els.enableNotifications.textContent = "Test tab alert";
553
+ } else {
554
+ els.enableNotifications.textContent = "Enable alerts";
555
+ }
556
+ }
557
+ function showSystemNotification(title) {
558
+ new Notification(title, { body: "Open Agent Social to view it." });
559
+ }
560
+ function playAlertTone() {
561
+ try {
562
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
563
+ if (!AudioContextClass) return false;
564
+ alertAudioContext = alertAudioContext || new AudioContextClass();
565
+ if (alertAudioContext.state === "suspended") alertAudioContext.resume();
566
+ const oscillator = alertAudioContext.createOscillator();
567
+ const gain = alertAudioContext.createGain();
568
+ const now = alertAudioContext.currentTime;
569
+ oscillator.type = "sine";
570
+ oscillator.frequency.setValueAtTime(660, now);
571
+ gain.gain.setValueAtTime(0.0001, now);
572
+ gain.gain.exponentialRampToValueAtTime(0.045, now + 0.01);
573
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.14);
574
+ oscillator.connect(gain);
575
+ gain.connect(alertAudioContext.destination);
576
+ oscillator.start(now);
577
+ oscillator.stop(now + 0.15);
578
+ return true;
579
+ } catch {
580
+ return false;
581
+ }
582
+ }
583
+ function triggerTabAlert() {
584
+ document.title = "● Agent Social";
585
+ return playAlertTone();
586
+ }
587
+ function clearTabAlert() {
588
+ if (!document.hidden) document.title = baseTitle;
589
+ }
590
+ function notifyIncomingMessage() {
591
+ if (window.Notification && Notification.permission === "granted") {
592
+ showSystemNotification("New Agent Social message");
593
+ } else if (fallbackAlertsEnabled) {
594
+ triggerTabAlert();
595
+ }
596
+ }
597
+ document.addEventListener("visibilitychange", () => { clearTabAlert(); maybeScheduleAutoAnalysis(); });
598
+ window.addEventListener("focus", () => { clearTabAlert(); maybeScheduleAutoAnalysis(); });
599
+
600
+ function maybeScheduleAutoAnalysis() {
601
+ const target = currentAnalysisTarget();
602
+ const outcome = target ? analysisOutcomes.get(target.contextHash) : undefined;
603
+ const shouldSchedule = embedded
604
+ && !document.hidden
605
+ && state.preferences.autoAnalyze !== false
606
+ && target
607
+ && target.autoEligible !== false
608
+ && target.status !== "ready"
609
+ && target.status !== "analyzing"
610
+ && analysisInFlightHash !== target.contextHash
611
+ && !outcome;
612
+ if (!shouldSchedule) {
613
+ if (analysisTimer) {
614
+ clearTimeout(analysisTimer);
615
+ analysisTimer = undefined;
616
+ analysisTimerHash = "";
617
+ }
618
+ return;
619
+ }
620
+ if (analysisTimer && analysisTimerHash === target.contextHash) return;
621
+ if (analysisTimer) clearTimeout(analysisTimer);
622
+ analysisTimerHash = target.contextHash;
623
+ analysisTimer = setTimeout(() => {
624
+ analysisTimer = undefined;
625
+ analysisTimerHash = "";
626
+ runReplyAnalysis(false);
627
+ }, 2000);
628
+ }
629
+
630
+ async function runReplyAnalysis(force) {
631
+ const target = currentAnalysisTarget();
632
+ if (!target || !embedded || analysisInFlightHash) return;
633
+ analysisInFlightHash = target.contextHash;
634
+ analysisOutcomes.set(target.contextHash, { status: "analyzing" });
635
+ render();
636
+ try {
637
+ const payload = await action("dating_ui_analyze_reply", {
638
+ sessionId: target.sessionId,
639
+ sourceMessageId: target.sourceMessageId,
640
+ contextHash: target.contextHash,
641
+ force: Boolean(force)
642
+ });
643
+ const status = payload.assistanceStatus || "failed";
644
+ analysisOutcomes.set(target.contextHash, {
645
+ status,
646
+ prompt: payload.fallbackPrompt || ""
647
+ });
648
+ applyPayload(payload);
649
+ } catch (error) {
650
+ analysisOutcomes.set(target.contextHash, { status: "failed" });
651
+ showError(error);
652
+ } finally {
653
+ analysisInFlightHash = "";
654
+ render();
655
+ }
656
+ }
657
+
658
+ async function requestVisibleAgentFallback(target, outcome) {
659
+ const prompt = outcome && outcome.prompt;
660
+ if (!prompt) return runReplyAnalysis(true);
661
+ try {
662
+ if (window.openai && typeof window.openai.sendFollowUpMessage === "function") {
663
+ await window.openai.sendFollowUpMessage({ prompt });
664
+ } else {
665
+ await sendRequest("ui/message", { role: "user", content: { type: "text", text: prompt } });
666
+ }
667
+ analysisOutcomes.set(target.contextHash, { status: "fallback-sent", prompt });
668
+ showToast("Agent analysis request sent to the main conversation");
669
+ render();
670
+ } catch (error) {
671
+ analysisOutcomes.set(target.contextHash, { status: "failed", prompt });
672
+ showError(error);
673
+ render();
674
+ }
675
+ }
676
+
677
+ function selectSuggestion(suggestion) {
678
+ if (!els.messageInput.value.trim()) {
679
+ els.messageInput.value = suggestion;
680
+ els.messageInput.focus();
681
+ return;
682
+ }
683
+ pendingSuggestion = suggestion;
684
+ els.suggestionPreview.textContent = "Your composer already has text. Replace it or append this suggestion?";
685
+ els.suggestionChoice.hidden = false;
686
+ }
687
+
688
+ function finishSuggestionChoice(mode) {
689
+ if (!pendingSuggestion) return;
690
+ if (mode === "replace") {
691
+ els.messageInput.value = pendingSuggestion;
692
+ }
693
+ if (mode === "append") {
694
+ els.messageInput.value = els.messageInput.value.trimEnd() + "\n" + pendingSuggestion;
695
+ }
696
+ pendingSuggestion = "";
697
+ els.suggestionChoice.hidden = true;
698
+ els.messageInput.focus();
699
+ }
700
+
701
+ async function sendCurrentMessage() {
702
+ const selected = selectedSession();
703
+ const body = els.messageInput.value.trim();
704
+ if (!selected || !body || sending) return;
705
+ sending = true;
706
+ render();
707
+ try {
708
+ const payload = await action("dating_ui_send", { sessionId: selected.id, body });
709
+ applyPayload(payload.state || payload);
710
+ els.messageInput.value = "";
711
+ showToast("Queued for delivery");
712
+ } catch (error) {
713
+ showError(error);
714
+ } finally {
715
+ sending = false;
716
+ render();
717
+ }
718
+ }
719
+
720
+ els.setupForm.addEventListener("submit", async (event) => {
721
+ event.preventDefault();
722
+ const credential = els.credentialInput.value.trim();
723
+ if (!credential) return;
724
+ try {
725
+ applyPayload(await api("/api/state", { method: "POST", body: { userToken: credential } }));
726
+ els.credentialInput.value = "";
727
+ applyPayload(await api("/api/messages/sync", { method: "POST", body: {} }));
728
+ } catch (error) { showError(error); }
729
+ });
730
+ els.syncNow.addEventListener("click", async () => {
731
+ try { applyPayload(await action("dating_ui_sync", {})); showToast("Synced"); } catch (error) { showError(error); }
732
+ });
733
+ els.composer.addEventListener("submit", async (event) => {
734
+ event.preventDefault();
735
+ await sendCurrentMessage();
736
+ });
737
+ els.messageInput.addEventListener("keydown", (event) => {
738
+ if (event.shiftKey && event.key === "Enter") {
739
+ event.preventDefault();
740
+ els.composer.requestSubmit();
741
+ }
742
+ });
743
+ els.autoAnalyze.addEventListener("change", async () => {
744
+ try {
745
+ applyPayload(await action("dating_ui_preferences", { autoAnalyze: els.autoAnalyze.checked }));
746
+ showToast(els.autoAnalyze.checked ? "Auto-analysis enabled" : "Auto-analysis disabled");
747
+ } catch (error) { showError(error); }
748
+ });
749
+ els.analyzeReply.addEventListener("click", async () => {
750
+ const target = currentAnalysisTarget();
751
+ if (!target) return;
752
+ const outcome = analysisOutcomes.get(target.contextHash);
753
+ if (outcome?.status === "fallback") {
754
+ await requestVisibleAgentFallback(target, outcome);
755
+ } else {
756
+ await runReplyAnalysis(true);
757
+ }
758
+ });
759
+ els.replaceSuggestion.addEventListener("click", () => finishSuggestionChoice("replace"));
760
+ els.appendSuggestion.addEventListener("click", () => finishSuggestionChoice("append"));
761
+ els.cancelSuggestion.addEventListener("click", () => finishSuggestionChoice("cancel"));
762
+ els.enableNotifications.addEventListener("click", async () => {
763
+ if (!window.Notification) {
764
+ fallbackAlertsEnabled = true;
765
+ const soundPlayed = triggerTabAlert();
766
+ updateAlertControl();
767
+ showToast(soundPlayed ? "Tab and sound alert tested" : "Tab alert tested — sound unavailable");
768
+ return;
769
+ }
770
+ const permission = Notification.permission === "granted"
771
+ ? "granted"
772
+ : await Notification.requestPermission();
773
+ if (permission === "granted") {
774
+ showSystemNotification("Agent Social alerts enabled");
775
+ updateAlertControl();
776
+ showToast("Test notification sent");
777
+ } else {
778
+ updateAlertControl();
779
+ showToast(permission === "denied" ? "Notifications are blocked in this browser" : "Notifications not enabled");
780
+ }
781
+ });
782
+ for (const [element, actionName] of [[els.reportSession, "report"], [els.blockSession, "block"], [els.closeSession, "close"]]) {
783
+ element.addEventListener("click", () => openConfirm(actionName));
784
+ }
785
+ function openConfirm(actionName) {
786
+ pendingAction = actionName;
787
+ const labels = {
788
+ report: ["Report this conversation?", "Reporting stops the conversation and preserves available safety context."],
789
+ block: ["Block this match?", "Blocking closes this conversation and prevents future matching."],
790
+ close: ["Close this conversation?", "You will no longer be able to send messages in this session."]
791
+ };
792
+ els.confirmTitle.textContent = labels[actionName][0];
793
+ els.confirmText.textContent = labels[actionName][1];
794
+ els.reportReasonWrap.hidden = actionName !== "report";
795
+ els.confirmAction.textContent = "Confirm";
796
+ els.confirmAction.className = "danger";
797
+ els.confirmDialog.showModal();
798
+ }
799
+ els.confirmDialog.addEventListener("close", async () => {
800
+ const actionName = pendingAction;
801
+ pendingAction = "";
802
+ if (els.confirmDialog.returnValue !== "confirm" || !actionName) {
803
+ els.reportReason.value = "";
804
+ return;
805
+ }
806
+ const selected = selectedSession();
807
+ if (!selected) return;
808
+ const toolName = "dating_ui_" + actionName;
809
+ const args = { sessionId: selected.id };
810
+ if (actionName === "report") args.reason = els.reportReason.value.trim();
811
+ try {
812
+ applyPayload(await action(toolName, args));
813
+ showToast(actionName + " completed");
814
+ }
815
+ catch (error) { showError(error); }
816
+ finally {
817
+ els.reportReason.value = "";
818
+ render();
819
+ }
820
+ });
821
+
822
+ function applyHostContext(context) {
823
+ if (context && context.theme) document.documentElement.style.colorScheme = context.theme;
824
+ }
825
+ async function initializeEmbedded() {
826
+ if (window.openai && window.openai.toolOutput) applyPayload(window.openai.toolOutput);
827
+ if (!window.openai) {
828
+ const result = await sendRequest("ui/initialize", {
829
+ protocolVersion: "2026-01-26",
830
+ appCapabilities: { availableDisplayModes: ["inline", "fullscreen"] },
831
+ clientInfo: { name: "agent-social-view", version: "0.2.0" }
832
+ });
833
+ applyHostContext(result && result.hostContext);
834
+ sendNotification("ui/notifications/initialized", {});
835
+ }
836
+ scheduleEmbeddedSync(0);
837
+ }
838
+ function scheduleEmbeddedSync(delay) {
839
+ setTimeout(async () => {
840
+ try {
841
+ applyPayload(await callEmbeddedTool("dating_ui_sync", {}));
842
+ embeddedRetryMs = 1000;
843
+ } catch (error) {
844
+ embeddedRetryMs = Math.min(embeddedRetryMs * 2, 15000);
845
+ showError(error);
846
+ }
847
+ scheduleEmbeddedSync(embeddedRetryMs);
848
+ }, delay);
849
+ }
850
+ async function initializeLocal() {
851
+ applyPayload(await api("/api/state"));
852
+ const events = new EventSource("/api/events");
853
+ events.addEventListener("state", (event) => applyPayload(JSON.parse(event.data)));
854
+ events.addEventListener("status", (event) => {
855
+ const status = JSON.parse(event.data);
856
+ els.connectionStatus.textContent = status.online ? "Connected · live" : "Reconnecting…";
857
+ els.connectionStatus.dataset.online = String(Boolean(status.online));
858
+ });
859
+ events.onerror = () => { els.connectionStatus.textContent = "Reconnecting…"; els.connectionStatus.dataset.online = "false"; };
860
+ }
861
+
862
+ updateAlertControl();
863
+ (embedded ? initializeEmbedded() : initializeLocal()).catch(showError);
864
+ </script>
865
+ </body>
866
+ </html>`;