@mindstone/mcp-server-microsoft-teams 0.1.2 → 0.3.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,968 @@
1
+ // GENERATED by scripts/gen-compose-html.mjs — do not edit by hand.
2
+ // Regenerate with "npm run compose:template"; "npm test" runs the --check drift gate.
3
+ // The generator also writes compose-message.html, keeping the two in sync.
4
+ export const COMPOSE_MESSAGE_HTML = `
5
+ <!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <title>Compose message</title>
11
+ <style>
12
+ :root {
13
+ --bg: #ffffff;
14
+ --panel: #ffffff;
15
+ --border: #d9dde5;
16
+ --text: #1f2937;
17
+ --muted: #6b7280;
18
+ --input-bg: #ffffff;
19
+ --button-bg: #1f6feb;
20
+ --button-text: #ffffff;
21
+ --button-bg-hover: #1b63d3;
22
+ --secondary-bg: #f3f4f6;
23
+ --secondary-text: #1f2937;
24
+ --secondary-border: #d1d5db;
25
+ --error-bg: #fef2f2;
26
+ --error-border: #fecaca;
27
+ --error-text: #991b1b;
28
+ --success-bg: #ecfdf5;
29
+ --success-border: #bbf7d0;
30
+ --success-text: #166534;
31
+ --warning-bg: #fffbeb;
32
+ --warning-border: #fde68a;
33
+ --warning-text: #92400e;
34
+ }
35
+
36
+ @media (prefers-color-scheme: dark) {
37
+ :root {
38
+ --bg: #0f172a;
39
+ --panel: #111827;
40
+ --border: #334155;
41
+ --text: #e5e7eb;
42
+ --muted: #9ca3af;
43
+ --input-bg: #0b1220;
44
+ --button-bg: #3b82f6;
45
+ --button-text: #eff6ff;
46
+ --button-bg-hover: #2563eb;
47
+ --secondary-bg: #1f2937;
48
+ --secondary-text: #e5e7eb;
49
+ --secondary-border: #374151;
50
+ --error-bg: #2b1011;
51
+ --error-border: #7f1d1d;
52
+ --error-text: #fca5a5;
53
+ --success-bg: #052e1b;
54
+ --success-border: #166534;
55
+ --success-text: #86efac;
56
+ --warning-bg: #2a2106;
57
+ --warning-border: #854d0e;
58
+ --warning-text: #fcd34d;
59
+ }
60
+ }
61
+
62
+ html[data-theme="light"] {
63
+ color-scheme: light;
64
+ }
65
+
66
+ html[data-theme="dark"] {
67
+ color-scheme: dark;
68
+ --bg: #0f172a;
69
+ --panel: #111827;
70
+ --border: #334155;
71
+ --text: #e5e7eb;
72
+ --muted: #9ca3af;
73
+ --input-bg: #0b1220;
74
+ --button-bg: #3b82f6;
75
+ --button-text: #eff6ff;
76
+ --button-bg-hover: #2563eb;
77
+ --secondary-bg: #1f2937;
78
+ --secondary-text: #e5e7eb;
79
+ --secondary-border: #374151;
80
+ --error-bg: #2b1011;
81
+ --error-border: #7f1d1d;
82
+ --error-text: #fca5a5;
83
+ --success-bg: #052e1b;
84
+ --success-border: #166534;
85
+ --success-text: #86efac;
86
+ --warning-bg: #2a2106;
87
+ --warning-border: #854d0e;
88
+ --warning-text: #fcd34d;
89
+ }
90
+
91
+ * {
92
+ box-sizing: border-box;
93
+ }
94
+
95
+ body {
96
+ margin: 0;
97
+ padding: 16px;
98
+ background: var(--bg);
99
+ color: var(--text);
100
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
101
+ line-height: 1.4;
102
+ }
103
+
104
+ .wrapper {
105
+ width: 100%;
106
+ max-width: 720px;
107
+ margin: 0 auto;
108
+ }
109
+
110
+ .card {
111
+ background: var(--panel);
112
+ border: 1px solid var(--border);
113
+ border-radius: 12px;
114
+ padding: 14px;
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 12px;
118
+ }
119
+
120
+ .field {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 6px;
124
+ }
125
+
126
+ .field label {
127
+ font-size: 12px;
128
+ font-weight: 600;
129
+ color: var(--muted);
130
+ text-transform: uppercase;
131
+ letter-spacing: 0.04em;
132
+ }
133
+
134
+ .from-value {
135
+ color: var(--text);
136
+ font-size: 14px;
137
+ padding: 2px 0;
138
+ word-break: break-word;
139
+ }
140
+
141
+ .from-helper {
142
+ font-size: 12px;
143
+ color: var(--muted);
144
+ margin-top: 2px;
145
+ }
146
+
147
+ .input,
148
+ .textarea {
149
+ width: 100%;
150
+ background: var(--input-bg);
151
+ color: var(--text);
152
+ border: 1px solid var(--border);
153
+ border-radius: 8px;
154
+ padding: 10px 12px;
155
+ font-size: 14px;
156
+ font-family: inherit;
157
+ }
158
+
159
+ .input:focus,
160
+ .textarea:focus {
161
+ outline: 2px solid rgba(59, 130, 246, 0.35);
162
+ outline-offset: 1px;
163
+ }
164
+
165
+ .textarea {
166
+ min-height: 180px;
167
+ resize: vertical;
168
+ white-space: pre-wrap;
169
+ }
170
+
171
+ .toggles {
172
+ display: flex;
173
+ gap: 12px;
174
+ align-items: center;
175
+ flex-wrap: wrap;
176
+ }
177
+
178
+ .link-button {
179
+ border: none;
180
+ padding: 0;
181
+ background: none;
182
+ color: var(--muted);
183
+ text-decoration: underline;
184
+ font-size: 12px;
185
+ cursor: pointer;
186
+ }
187
+
188
+ .link-button:hover {
189
+ color: var(--text);
190
+ }
191
+
192
+ .actions {
193
+ display: flex;
194
+ justify-content: flex-end;
195
+ gap: 10px;
196
+ margin-top: 2px;
197
+ }
198
+
199
+ .button {
200
+ border-radius: 8px;
201
+ border: 1px solid transparent;
202
+ min-height: 36px;
203
+ padding: 0 14px;
204
+ font-size: 13px;
205
+ font-weight: 600;
206
+ font-family: inherit;
207
+ cursor: pointer;
208
+ display: inline-flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ gap: 8px;
212
+ transition: background 120ms ease, opacity 120ms ease, border-color 120ms ease;
213
+ }
214
+
215
+ .button:disabled {
216
+ cursor: not-allowed;
217
+ opacity: 0.75;
218
+ }
219
+
220
+ .button-primary {
221
+ background: var(--button-bg);
222
+ color: var(--button-text);
223
+ }
224
+
225
+ .button-primary:hover:enabled {
226
+ background: var(--button-bg-hover);
227
+ }
228
+
229
+ .button-secondary {
230
+ background: var(--secondary-bg);
231
+ color: var(--secondary-text);
232
+ border-color: var(--secondary-border);
233
+ }
234
+
235
+ .spinner {
236
+ width: 14px;
237
+ height: 14px;
238
+ border: 2px solid currentColor;
239
+ border-right-color: transparent;
240
+ border-radius: 50%;
241
+ display: inline-block;
242
+ animation: spin 700ms linear infinite;
243
+ }
244
+
245
+ @keyframes spin {
246
+ to { transform: rotate(360deg); }
247
+ }
248
+
249
+ .status {
250
+ border-radius: 8px;
251
+ border: 1px solid transparent;
252
+ padding: 8px 10px;
253
+ font-size: 13px;
254
+ }
255
+
256
+ .error {
257
+ background: var(--error-bg);
258
+ border-color: var(--error-border);
259
+ color: var(--error-text);
260
+ display: flex;
261
+ justify-content: space-between;
262
+ align-items: center;
263
+ gap: 8px;
264
+ flex-wrap: wrap;
265
+ }
266
+
267
+ .success {
268
+ background: var(--success-bg);
269
+ border-color: var(--success-border);
270
+ color: var(--success-text);
271
+ }
272
+
273
+ .warning {
274
+ background: var(--warning-bg);
275
+ border-color: var(--warning-border);
276
+ color: var(--warning-text);
277
+ display: block;
278
+ }
279
+
280
+ .warning strong {
281
+ display: block;
282
+ margin-bottom: 2px;
283
+ }
284
+
285
+ .warning .warning-detail {
286
+ opacity: 0.85;
287
+ }
288
+
289
+ .collapsed {
290
+ border: 1px solid var(--border);
291
+ border-radius: 10px;
292
+ background: var(--panel);
293
+ padding: 10px 12px;
294
+ display: flex;
295
+ justify-content: space-between;
296
+ align-items: center;
297
+ gap: 8px;
298
+ font-size: 13px;
299
+ }
300
+
301
+ .hidden {
302
+ display: none !important;
303
+ }
304
+
305
+ .sent-header {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 8px;
309
+ flex-wrap: wrap;
310
+ }
311
+
312
+ .sent-badge {
313
+ display: inline-flex;
314
+ align-items: center;
315
+ background: var(--success-bg);
316
+ color: var(--success-text);
317
+ border: 1px solid var(--success-border);
318
+ border-radius: 999px;
319
+ padding: 2px 10px;
320
+ font-size: 12px;
321
+ font-weight: 600;
322
+ }
323
+
324
+ .sent-timestamp {
325
+ color: var(--muted);
326
+ font-size: 12px;
327
+ }
328
+
329
+ .sent-value {
330
+ color: var(--text);
331
+ font-size: 14px;
332
+ word-break: break-word;
333
+ }
334
+
335
+ .sent-body {
336
+ color: var(--text);
337
+ font-size: 14px;
338
+ white-space: pre-wrap;
339
+ word-break: break-word;
340
+ max-height: 260px;
341
+ overflow: auto;
342
+ border: 1px solid var(--border);
343
+ border-radius: 8px;
344
+ padding: 10px 12px;
345
+ background: var(--input-bg);
346
+ }
347
+ </style>
348
+ </head>
349
+ <body>
350
+ <div class="wrapper">
351
+ <div id="collapsedState" class="collapsed hidden">
352
+ <span id="collapsedMessage">Draft collapsed.</span>
353
+ <button id="reopenButton" type="button" class="button button-secondary">Reopen</button>
354
+ </div>
355
+
356
+ <form id="composeForm" class="card" novalidate aria-label="Compose message">
357
+ <div class="field">
358
+ <span id="workspaceLine" class="from-helper hidden"></span>
359
+ </div>
360
+
361
+ <div class="field">
362
+ <label for="toInput">To</label>
363
+ <input id="toInput" class="input" type="text" autocomplete="off" placeholder="Chat ID">
364
+ </div>
365
+
366
+ <div class="field">
367
+ <label for="bodyInput">Message</label>
368
+ <textarea id="bodyInput" class="textarea" placeholder="Write your message"></textarea>
369
+ </div>
370
+
371
+ <div id="errorBox" class="status error hidden" role="alert">
372
+ <span id="errorText"></span>
373
+ <button id="retryButton" type="button" class="link-button">Retry</button>
374
+ </div>
375
+
376
+ <div id="successBox" class="status success hidden" role="status" aria-live="polite"></div>
377
+
378
+ <div id="unknownBox" class="status warning hidden" role="status" aria-live="polite">
379
+ <strong id="unknownTitle"></strong>
380
+ <span id="unknownText" class="warning-detail"></span>
381
+ </div>
382
+
383
+ <div class="actions">
384
+ <button id="cancelButton" type="button" class="button button-secondary">Cancel</button>
385
+ <button id="sendButton" type="button" class="button button-primary">
386
+ <span id="sendSpinner" class="spinner hidden" aria-hidden="true"></span>
387
+ <span id="sendLabel">Send message</span>
388
+ </button>
389
+ </div>
390
+ </form>
391
+
392
+ <div id="sentView" class="card hidden" role="status" aria-live="polite">
393
+ <div class="sent-header">
394
+ <span class="sent-badge">Sent</span>
395
+ <span id="sentTimestamp" class="sent-timestamp"></span>
396
+ </div>
397
+
398
+ <div class="field">
399
+ <label>To</label>
400
+ <span id="sentTo" class="sent-value"></span>
401
+ </div>
402
+
403
+ <div class="field">
404
+ <label>Message</label>
405
+ <div id="sentBody" class="sent-body"></div>
406
+ </div>
407
+
408
+ <div class="actions">
409
+ <button id="sentCollapseButton" type="button" class="button button-secondary">Collapse</button>
410
+ </div>
411
+ </div>
412
+ </div>
413
+
414
+ <script>
415
+ (function () {
416
+ 'use strict';
417
+
418
+ var RESOURCE_URI = 'ui://microsoft-teams/compose-message';
419
+ var currentEmail = '';
420
+ // The host prefills the draft exactly once via ui/notifications/tool-result
421
+ // on ready. We apply only the FIRST prefill and ignore any later ones, so a
422
+ // stray or hostile re-post can't silently rewrite a draft the user has
423
+ // already reviewed before clicking Send.
424
+ var draftApplied = false;
425
+ var pendingRequestId = null;
426
+ var pendingPayload = null;
427
+ var sending = false;
428
+ var collapsed = false;
429
+ var outcomeUnknown = false;
430
+ var sent = false;
431
+ var sendTimeoutId = null;
432
+ // Just beyond the host-side tool-call timeout (60s) so a legitimately slow
433
+ // send resolves to its real result first; this only fires when the reply
434
+ // is genuinely lost, so the button can never stay stuck-silent.
435
+ var SEND_TIMEOUT_MS = 75000;
436
+
437
+ var composeForm = document.getElementById('composeForm');
438
+ var collapsedState = document.getElementById('collapsedState');
439
+ var collapsedMessage = document.getElementById('collapsedMessage');
440
+ var reopenButton = document.getElementById('reopenButton');
441
+
442
+ var sentView = document.getElementById('sentView');
443
+ var sentTimestamp = document.getElementById('sentTimestamp');
444
+ var sentFrom = document.getElementById('sentFrom');
445
+ var sentTo = document.getElementById('sentTo');
446
+ var sentSubject = document.getElementById('sentSubject');
447
+ var sentBody = document.getElementById('sentBody');
448
+ var sentCollapseButton = document.getElementById('sentCollapseButton');
449
+
450
+ var toInput = document.getElementById('toInput');
451
+ var subjectInput = document.getElementById('subjectInput');
452
+ var bodyInput = document.getElementById('bodyInput');
453
+ var fromValue = document.getElementById('fromValue');
454
+ var fromHelper = document.getElementById('fromHelper');
455
+
456
+ var sendButton = document.getElementById('sendButton');
457
+ var sendLabel = document.getElementById('sendLabel');
458
+ var sendSpinner = document.getElementById('sendSpinner');
459
+ var cancelButton = document.getElementById('cancelButton');
460
+ var retryButton = document.getElementById('retryButton');
461
+
462
+ var errorBox = document.getElementById('errorBox');
463
+ var errorText = document.getElementById('errorText');
464
+ var successBox = document.getElementById('successBox');
465
+ var unknownBox = document.getElementById('unknownBox');
466
+ var unknownTitle = document.getElementById('unknownTitle');
467
+ var unknownText = document.getElementById('unknownText');
468
+
469
+ function applyThemeFromHostContext() {
470
+ try {
471
+ var context = window.__MCP_HOST_CONTEXT__;
472
+ if (!context || (context.theme !== 'light' && context.theme !== 'dark')) {
473
+ return;
474
+ }
475
+ document.documentElement.setAttribute('data-theme', context.theme);
476
+ } catch (_) {
477
+ // Ignore theme extraction errors
478
+ }
479
+ }
480
+
481
+ function trimString(value) {
482
+ return String(value || '').trim();
483
+ }
484
+
485
+ function applyFromValue(email) {
486
+ var trimmed = trimString(email);
487
+ if (trimmed) {
488
+ fromValue.textContent = trimmed;
489
+ fromValue.style.color = '';
490
+ fromHelper.textContent = '';
491
+ fromHelper.classList.add('hidden');
492
+ } else {
493
+ fromValue.textContent = 'Account not shown';
494
+ fromValue.style.color = 'var(--muted)';
495
+ fromHelper.textContent = '';
496
+ fromHelper.classList.remove('hidden');
497
+ }
498
+ }
499
+
500
+ function formatAddressItem(item) {
501
+ if (item && typeof item === 'object') {
502
+ var email = trimString(item.email || item.address || item.value);
503
+ var name = trimString(item.name || item.displayName);
504
+ if (email && name) {
505
+ return name + ' <' + email + '>';
506
+ }
507
+ return email;
508
+ }
509
+ return trimString(item);
510
+ }
511
+
512
+ function normalizeAddressList(value) {
513
+ if (Array.isArray(value)) {
514
+ return value.map(formatAddressItem).filter(Boolean);
515
+ }
516
+ if (typeof value === 'string') {
517
+ return value
518
+ .split(',')
519
+ .map(function (item) { return trimString(item); })
520
+ .filter(Boolean);
521
+ }
522
+ return [];
523
+ }
524
+
525
+ function listToInputValue(list) {
526
+ return normalizeAddressList(list).join(', ');
527
+ }
528
+
529
+ var resizeQueued = false;
530
+ function postResize() {
531
+ if (resizeQueued) return;
532
+ resizeQueued = true;
533
+ requestAnimationFrame(function () {
534
+ resizeQueued = false;
535
+ var height = Math.max(
536
+ document.documentElement.scrollHeight,
537
+ document.body ? document.body.scrollHeight : 0
538
+ );
539
+ window.parent.postMessage(
540
+ { jsonrpc: '2.0', method: 'ui/resize', params: { height: height } },
541
+ '*'
542
+ );
543
+ });
544
+ }
545
+
546
+ function postReady() {
547
+ window.parent.postMessage(
548
+ {
549
+ method: 'mcp-app:ready',
550
+ params: { resourceUri: RESOURCE_URI }
551
+ },
552
+ '*'
553
+ );
554
+ }
555
+
556
+ // Exactly one of the three panels is visible at a time: the editable form,
557
+ // the read-only sent view, or the collapsed summary bar.
558
+ function showView(view) {
559
+ composeForm.classList.toggle('hidden', view !== 'form');
560
+ sentView.classList.toggle('hidden', view !== 'sent');
561
+ collapsedState.classList.toggle('hidden', view !== 'collapsed');
562
+ postResize();
563
+ }
564
+
565
+ function setCollapsed(nextCollapsed, message) {
566
+ collapsed = nextCollapsed;
567
+ if (nextCollapsed) {
568
+ collapsedMessage.textContent = message || 'Draft collapsed.';
569
+ showView('collapsed');
570
+ } else {
571
+ // Expanding after a send restores the read-only sent view (never the
572
+ // editable form) so the user can re-read what went out without any risk
573
+ // of a duplicate send; before sending, it restores the editable form.
574
+ showView(sent ? 'sent' : 'form');
575
+ }
576
+ }
577
+
578
+ function setSending(nextSending) {
579
+ sending = nextSending;
580
+ sendButton.disabled = nextSending;
581
+ cancelButton.disabled = nextSending;
582
+ toInput.disabled = nextSending;
583
+ bodyInput.disabled = nextSending;
584
+ sendSpinner.classList.toggle('hidden', !nextSending);
585
+ sendLabel.textContent = nextSending ? 'Sending…' : 'Send message';
586
+ sendButton.setAttribute('aria-busy', nextSending ? 'true' : 'false');
587
+ postResize();
588
+ }
589
+
590
+ function clearError() {
591
+ errorText.textContent = '';
592
+ errorBox.classList.add('hidden');
593
+ }
594
+
595
+ function showError(message) {
596
+ errorText.textContent = message || 'Failed to send message.';
597
+ errorBox.classList.remove('hidden');
598
+ postResize();
599
+ }
600
+
601
+ function clearSuccess() {
602
+ successBox.textContent = '';
603
+ successBox.classList.add('hidden');
604
+ }
605
+
606
+ function showSuccess(message) {
607
+ successBox.textContent = message;
608
+ successBox.classList.remove('hidden');
609
+ postResize();
610
+ }
611
+
612
+ function clearSendTimeout() {
613
+ if (sendTimeoutId !== null) {
614
+ clearTimeout(sendTimeoutId);
615
+ sendTimeoutId = null;
616
+ }
617
+ }
618
+
619
+ function clearUnknown() {
620
+ outcomeUnknown = false;
621
+ unknownTitle.textContent = '';
622
+ unknownText.textContent = '';
623
+ unknownBox.classList.add('hidden');
624
+ }
625
+
626
+ // No reply ever came back for the send (e.g. the host's response was lost
627
+ // before reaching this iframe). We genuinely don't know whether the email
628
+ // went out, so we must NOT claim success or failure: re-enable the form and
629
+ // tell the user to verify before resending (the send is not idempotent, so
630
+ // a blind retry could send a duplicate).
631
+ function showSendUnknown() {
632
+ outcomeUnknown = true;
633
+ clearError();
634
+ clearSuccess();
635
+ unknownTitle.textContent = 'Not sure if this sent.';
636
+ unknownText.textContent = "Rebel didn't hear back in time. Check whether it sent before sending again, so it doesn't go out twice.";
637
+ unknownBox.classList.remove('hidden');
638
+ postResize();
639
+ }
640
+
641
+ function handleSendTimeout() {
642
+ sendTimeoutId = null;
643
+ if (!pendingRequestId) {
644
+ return;
645
+ }
646
+ pendingRequestId = null;
647
+ setSending(false);
648
+ showSendUnknown();
649
+ }
650
+
651
+ function readFormPayload() {
652
+ var chatId = String(toInput.value || '').trim();
653
+ var payload = {
654
+ chatId: chatId,
655
+ content: String(bodyInput.value || '')
656
+ };
657
+ return payload;
658
+ }
659
+
660
+ function validatePayload(payload) {
661
+ if (!trimString(payload.chatId)) {
662
+ return 'Add a chat in To.';
663
+ }
664
+ if (!trimString(payload.content)) {
665
+ return 'Add a message before sending.';
666
+ }
667
+ return null;
668
+ }
669
+
670
+ function sendPayload(payload) {
671
+ pendingPayload = payload;
672
+ pendingRequestId = 'send-' + Date.now() + '-' + Math.random().toString(16).slice(2);
673
+ setSending(true);
674
+ clearError();
675
+ clearSuccess();
676
+ clearUnknown();
677
+ clearSendTimeout();
678
+ sendTimeoutId = setTimeout(handleSendTimeout, SEND_TIMEOUT_MS);
679
+
680
+ window.parent.postMessage(
681
+ {
682
+ jsonrpc: '2.0',
683
+ method: 'tools/call',
684
+ params: {
685
+ name: 'send_chat_message',
686
+ arguments: payload
687
+ },
688
+ id: pendingRequestId
689
+ },
690
+ '*'
691
+ );
692
+ }
693
+
694
+ function applyDraftData(rawDraft) {
695
+ var draft = rawDraft && typeof rawDraft === 'object' ? rawDraft : {};
696
+ toInput.value = String(draft.target || '');
697
+ bodyInput.value = String(draft.text || '');
698
+ clearError();
699
+ clearSuccess();
700
+ postResize();
701
+ }
702
+
703
+ function getDraftFromToolResult(payload) {
704
+ if (!payload || typeof payload !== 'object') {
705
+ return null;
706
+ }
707
+ // CANONICAL: post-A0 super-mcp hoists structuredContent onto the
708
+ // outer block; the host posts it through \`params.structuredContent\`.
709
+ if (payload.structuredContent && typeof payload.structuredContent === 'object') {
710
+ return payload.structuredContent;
711
+ }
712
+ // MIGRATION SHIM: pre-A0 stored sessions replayed through the
713
+ // current iframe lack \`params.structuredContent\` because the outer
714
+ // block had no hoist at capture time. Read inner envelope text →
715
+ // JSON.parse → result.structuredContent. NOT regex-based.
716
+ //
717
+ // Lifetime: PERMANENT defensive read-side helper. Pre-A0 sessions
718
+ // are persisted on disk and may be replayed indefinitely; there is
719
+ // no time-based sunset. Removing this fallback would silently break
720
+ // pre-A0 conversation replay.
721
+ var text = typeof payload.text === 'string' ? payload.text :
722
+ (Array.isArray(payload.content)
723
+ ? (payload.content.find(function (c) { return c && c.type === 'text' && typeof c.text === 'string'; }) || {}).text
724
+ : null);
725
+ if (typeof text !== 'string') return null;
726
+ try {
727
+ var parsed = JSON.parse(text);
728
+ if (parsed && typeof parsed === 'object' && parsed.result && typeof parsed.result === 'object') {
729
+ var inner = parsed.result;
730
+ if (inner.structuredContent && typeof inner.structuredContent === 'object') {
731
+ return inner.structuredContent;
732
+ }
733
+ }
734
+ } catch (_) {
735
+ // Likely-envelope heuristic: text starts with \`{\` and contains
736
+ // \`"package_id"\` in the first 64 chars. If JSON.parse failed on a
737
+ // likely-envelope, surface a warning so the developer chasing
738
+ // "compose form is empty on replay" sees a signal. For non-envelope
739
+ // text (the dominant case), no log fires.
740
+ var prefix = text.length > 64 ? text.slice(0, 64) : text;
741
+ var likelyEnvelope = prefix.charAt(0) === '{' && prefix.indexOf('"package_id"') !== -1;
742
+ if (likelyEnvelope && typeof console !== 'undefined' && typeof console.warn === 'function') {
743
+ console.warn('[compose-email] Migration shim: JSON.parse failed on likely super-mcp envelope; pre-fill skipped');
744
+ }
745
+ }
746
+ return null;
747
+ }
748
+
749
+ function findTextBlock(blocks) {
750
+ if (!Array.isArray(blocks)) return null;
751
+ var block = blocks.find(function (c) {
752
+ return c && c.type === 'text' && typeof c.text === 'string';
753
+ });
754
+ return block ? block.text : null;
755
+ }
756
+
757
+ // Extract the send result identifiers ({ messageId, threadId, labelIds })
758
+ // from the tool-call reply. The connector attaches structuredContent, which
759
+ // super-mcp hoists onto the outer block, so the fast path is a single read.
760
+ // The envelope-unwrapping fallback keeps this robust for hosts (or replayed
761
+ // sessions) that forward only the serialized super-mcp text envelope.
762
+ function getSendMetaFromResult(result) {
763
+ if (!result || typeof result !== 'object') return null;
764
+ if (result.structuredContent && typeof result.structuredContent === 'object') {
765
+ return result.structuredContent;
766
+ }
767
+ try {
768
+ var outerText = findTextBlock(result.content);
769
+ if (typeof outerText !== 'string' && typeof result.text === 'string') {
770
+ outerText = result.text;
771
+ }
772
+ if (typeof outerText !== 'string') return null;
773
+ var envelope = JSON.parse(outerText);
774
+ if (!envelope || typeof envelope !== 'object') return null;
775
+ // outerText was the connector payload directly (no super-mcp envelope).
776
+ if (envelope.messageId || envelope.threadId) return envelope;
777
+ var inner = envelope.result;
778
+ if (!inner || typeof inner !== 'object') return null;
779
+ if (inner.structuredContent && typeof inner.structuredContent === 'object') {
780
+ return inner.structuredContent;
781
+ }
782
+ var innerText = findTextBlock(inner.content);
783
+ if (typeof innerText !== 'string') return null;
784
+ var payload = JSON.parse(innerText);
785
+ return (payload && typeof payload === 'object') ? payload : null;
786
+ } catch (_) {
787
+ return null;
788
+ }
789
+ }
790
+
791
+ function formatSentTime() {
792
+ try {
793
+ return new Date().toLocaleString(undefined, {
794
+ month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit'
795
+ });
796
+ } catch (_) {
797
+ return '';
798
+ }
799
+ }
800
+
801
+ function renderSentView(payload, meta, stamp) {
802
+ payload = payload && typeof payload === 'object' ? payload : {};
803
+ sentTimestamp.textContent = stamp ? 'Sent ' + stamp : 'Sent';
804
+ sentTo.textContent = String(payload.channel || payload.chatId || '');
805
+ sentBody.textContent = String(payload.text || payload.content || '');
806
+ }
807
+
808
+ function handleSendRequest() {
809
+ if (sending) return;
810
+
811
+ var payload = readFormPayload();
812
+ var validationError = validatePayload(payload);
813
+ if (validationError) {
814
+ showError(validationError);
815
+ return;
816
+ }
817
+
818
+ sendPayload(payload);
819
+ }
820
+
821
+ // The Send button is type="button" with an explicit click handler rather
822
+ // than a form submit button. The Rebel host renders this iframe with a
823
+ // sandbox of "allow-scripts" only — no "allow-forms" — so native form
824
+ // submission is blocked by the browser BEFORE the submit event fires
825
+ // ("Blocked form submission ... the form's frame is sandboxed and the
826
+ // allow-forms permission is not set"). A submit-driven Send therefore
827
+ // silently did nothing: no postMessage, no spinner, no telemetry.
828
+ // A click handler needs no sandbox permission and is the same path
829
+ // Cancel/Retry already use. The submit listener below stays only as a
830
+ // defensive fallback for hosts that DO grant allow-forms; in this sandbox
831
+ // it never fires (neither the button click nor Enter routes through it).
832
+ // The sending guard keeps any double-fire (in an allow-forms host) a no-op.
833
+ sendButton.addEventListener('click', function () {
834
+ handleSendRequest();
835
+ });
836
+
837
+ composeForm.addEventListener('submit', function (event) {
838
+ event.preventDefault();
839
+ handleSendRequest();
840
+ });
841
+
842
+ retryButton.addEventListener('click', function () {
843
+ if (sending) return;
844
+ var payload = readFormPayload();
845
+ var validationError = validatePayload(payload);
846
+ if (validationError) {
847
+ showError(validationError);
848
+ return;
849
+ }
850
+ sendPayload(payload);
851
+ });
852
+
853
+ cancelButton.addEventListener('click', function () {
854
+ if (sending) return;
855
+ // After a lost-reply timeout we don't know if the email sent, so don't
856
+ // claim "Nothing sent." on collapse — point the user to verify instead.
857
+ var collapseMessage = outcomeUnknown
858
+ ? 'Collapsed. Check whether it sent if you are not sure.'
859
+ : 'Draft collapsed. Nothing sent.';
860
+ clearError();
861
+ clearSuccess();
862
+ clearUnknown();
863
+ setCollapsed(true, collapseMessage);
864
+ });
865
+
866
+ reopenButton.addEventListener('click', function () {
867
+ setCollapsed(false);
868
+ });
869
+
870
+ sentCollapseButton.addEventListener('click', function () {
871
+ setCollapsed(true, collapsedMessage.textContent || 'Message sent.');
872
+ });
873
+
874
+ [toInput, bodyInput].forEach(function (element) {
875
+ element.addEventListener('input', function () {
876
+ clearError();
877
+ clearSuccess();
878
+ clearUnknown();
879
+ postResize();
880
+ });
881
+ });
882
+
883
+ window.addEventListener('message', function (event) {
884
+ // Honest scope (origin-independent): the iframe sandbox origin is 'null',
885
+ // so validating event.origin is unreliable across dev/prod/custom-scheme
886
+ // hosts. Instead reject any *identified* sender that isn't our host frame
887
+ // (window.parent). A real cross-frame postMessage always stamps
888
+ // event.source with the sending window, so a hostile sibling frame is
889
+ // rejected here; a null source only occurs for same-context synthetic
890
+ // dispatch (our own code) and cannot be forged from outside, so we
891
+ // tolerate it. This does not authenticate the host's identity — the host
892
+ // separately authenticates this iframe by origin+source.
893
+ if (event.source && event.source !== window.parent) {
894
+ return;
895
+ }
896
+
897
+ var data = event.data;
898
+ if (!data || typeof data !== 'object') {
899
+ return;
900
+ }
901
+
902
+ if (data.method === 'ui/notifications/tool-result') {
903
+ // Always parse (so the migration-shim's malformed-envelope warning
904
+ // still fires), but apply only the first successful prefill: a stray
905
+ // or hostile re-post can't silently rewrite a draft the user has
906
+ // already reviewed.
907
+ var draft = getDraftFromToolResult(data.params);
908
+ if (draft && !draftApplied) {
909
+ draftApplied = true;
910
+ applyDraftData(draft);
911
+ }
912
+ return;
913
+ }
914
+
915
+ if (data.jsonrpc !== '2.0' || !pendingRequestId || data.id !== pendingRequestId) {
916
+ return;
917
+ }
918
+
919
+ pendingRequestId = null;
920
+ clearSendTimeout();
921
+ clearUnknown();
922
+ setSending(false);
923
+
924
+ if (data.error) {
925
+ var errorMessage = data.error && typeof data.error.message === 'string'
926
+ ? data.error.message
927
+ : 'Failed to send message.';
928
+ showError(errorMessage);
929
+ return;
930
+ }
931
+
932
+ clearError();
933
+ clearSuccess();
934
+ // Preserve what was sent instead of discarding the draft. pendingPayload
935
+ // is the exact payload we posted. We default to the collapsed summary, but
936
+ // Reopen now restores a read-only view of the sent message.
937
+ var sentPayload = pendingPayload || readFormPayload();
938
+ var stamp = formatSentTime();
939
+ renderSentView(sentPayload, getSendMetaFromResult(data.result), stamp);
940
+ sent = true;
941
+ setCollapsed(true, stamp ? 'Message sent · ' + stamp : 'Message sent.');
942
+ });
943
+
944
+ if (typeof ResizeObserver === 'function' && document.body) {
945
+ var observer = new ResizeObserver(postResize);
946
+ observer.observe(document.body);
947
+ }
948
+
949
+ applyThemeFromHostContext();
950
+ setCollapsed(false);
951
+ postResize();
952
+ postReady();
953
+
954
+ window.parent.postMessage(
955
+ {
956
+ jsonrpc: '2.0',
957
+ method: 'ui/initialize',
958
+ params: { resourceUri: RESOURCE_URI },
959
+ id: 'compose-email-init'
960
+ },
961
+ '*'
962
+ );
963
+ })();
964
+ </script>
965
+ </body>
966
+ </html>
967
+ `;
968
+ //# sourceMappingURL=compose-message-template.js.map